pref: 优化GUI更新功能;项目分为gui和upgrade两个模块

This commit is contained in:
2024-10-30 00:11:41 +08:00
parent 4dc13bb8f1
commit 9bbc3f488f
94 changed files with 1056 additions and 313 deletions

315
gui/pom.xml Normal file
View File

@ -0,0 +1,315 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.octopusyan</groupId>
<artifactId>alist-gui</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>gui</artifactId>
<version>${parent.version}</version>
<name>alist-gui</name>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<exec.mainClass>cn.octopusyan.alistgui.AppLauncher</exec.mainClass>
<cssSrcPath>${project.basedir}/src/main/resources/css</cssSrcPath>
<cssTargetPath>${project.basedir}/target/classes/css</cssTargetPath>
</properties>
<dependencies>
<!-- javafx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
</dependency>
<!-- https://mkpaz.github.io/atlantafx/ -->
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
</dependency>
<!-- slf4j -->
<!-- https://slf4j.org/manual.html -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<!-- common -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<!-- https://kordamp.org/ikonli/ -->
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-fontawesome-pack</artifactId>
</dependency>
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>emoji</artifactId>
</dependency>
</dependencies>
<pluginRepositories>
<pluginRepository>
<id>nexus</id>
<name>nexus-snapshot-repository</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>css/*.scss</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>--enable-preview</compilerArgs>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- https://github.com/HebiRobotics/sass-cli-maven-plugin -->
<plugin>
<groupId>us.hebi.sass</groupId>
<artifactId>sass-cli-maven-plugin</artifactId>
<version>1.0.3</version>
<configuration>
<sassVersion>1.78.0</sassVersion>
<args> <!-- Any argument that should be forwarded to the sass cli -->
<arg>${cssSrcPath}/root.scss:${cssTargetPath}/root.css</arg>
<arg>${cssSrcPath}/root-view.scss:${cssTargetPath}/root-view.css</arg>
<arg>${cssSrcPath}/main-view.scss:${cssTargetPath}/main-view.css</arg>
<arg>${cssSrcPath}/setup-view.scss:${cssTargetPath}/setup-view.css</arg>
<arg>${cssSrcPath}/about-view.scss:${cssTargetPath}/about-view.css</arg>
<arg>${cssSrcPath}/admin-panel.scss:${cssTargetPath}/admin-panel.css</arg>
<arg>--no-source-map</arg>
</args>
</configuration>
<executions>
<execution>
<id>sass-exec</id>
<phase>generate-resources</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<stripDebug>true</stripDebug>
<compress>2</compress>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
<launcher>alistgui</launcher>
<jlinkImageName>app</jlinkImageName>
<jlinkZipName>app</jlinkZipName>
<mainClass>cn.octopusyan.alistgui/${exec.mainClass}</mainClass>
</configuration>
<executions>
<execution>
<!-- Default configuration for running with: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<stripDebug>true</stripDebug>
<compress>2</compress>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
<launcher>alist-gui</launcher>
<jlinkImageName>app</jlinkImageName>
<jlinkZipName>app</jlinkZipName>
<mainClass>cn.octopusyan.alistgui/${exec.mainClass}</mainClass>
<options>
<option>--enable-preview</option>
<!-- <option>-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005</option>-->
</options>
</configuration>
</execution>
</executions>
</plugin>
<!-- https://github.com/fvarrui/JavaPackager -->
<plugin>
<groupId>io.github.fvarrui</groupId>
<artifactId>javapackager</artifactId>
<version>1.7.7-SNAPSHOT</version>
<configuration>
<mainClass>${exec.mainClass}</mainClass>
<bundleJre>true</bundleJre>
<generateInstaller>false</generateInstaller>
<copyDependencies>true</copyDependencies>
<assetsDir>${project.basedir}/src/main/resources/assets</assetsDir>
<vmArgs>
<arg>--enable-preview</arg>
<arg>-Xmx100m</arg>
</vmArgs>
</configuration>
<executions>
<execution>
<id>windows</id>
<phase>package</phase>
<goals>
<goal>package</goal>
</goals>
<configuration>
<platform>windows</platform>
<zipballName>${project.name}-windows</zipballName>
<createZipball>true</createZipball>
<winConfig>
<headerType>gui</headerType>
<generateMsi>false</generateMsi>
</winConfig>
<additionalResources>
<additionalResource>${project.basedir}/src/main/resources/static/upgrade.exe
</additionalResource>
</additionalResources>
</configuration>
</execution>
<execution>
<id>windows-nojre</id>
<phase>package</phase>
<goals>
<goal>package</goal>
</goals>
<configuration>
<zipballName>${project.name}-windows-nojre</zipballName>
<platform>windows</platform>
<createZipball>true</createZipball>
<bundleJre>false</bundleJre>
<winConfig>
<headerType>gui</headerType>
<generateMsi>false</generateMsi>
</winConfig>
<additionalResources>
<additionalResource>${project.basedir}/src/main/resources/static/upgrade.exe
</additionalResource>
</additionalResources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>exe</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
<executions>
<execution>
<id>copy-resources</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<encoding>UTF-8</encoding>
<!--打成jar包后复制到的路径-->
<outputDirectory>../target</outputDirectory>
<resources>
<resource>
<!--项目中需要复制的文件路径-->
<directory>${project.basedir}/target</directory>
<includes>
<include>*.zip</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
package cn.octopusyan.alistgui;
/**
* 启动类
*
* @author octopus_yan@foxmail.com
*/
public class AppLauncher {
public static void main(String[] args) {
Application.launch(Application.class, args);
}
}

View File

@ -0,0 +1,221 @@
package cn.octopusyan.alistgui;
import cn.hutool.core.io.FileUtil;
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;
import cn.octopusyan.alistgui.model.upgrade.Gui;
import cn.octopusyan.alistgui.util.ProcessesUtil;
import cn.octopusyan.alistgui.view.alert.AlertUtil;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.*;
import java.net.http.HttpClient;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
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);
@Getter
private static Stage primaryStage;
@Override
public void init() throws Exception {
logger.info("application init ...");
long delay = 0L;
// 更新重启检查
File upgradeFile = new File(Constants.DATA_DIR_PATH + File.separator + new Gui().getReleaseFile());
logger.error("{}{}{}", Constants.DATA_DIR_PATH, File.separator, new Gui().getReleaseFile());
if (upgradeFile.exists()) {
logger.error("upgradeFile.exists");
FileUtil.del(upgradeFile);
delay = 1000;
}
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 单例模式检查
makeSingle();
}
}, delay);
// 初始化客户端配置
ConfigManager.load();
// http请求工具初始化
HttpConfig httpConfig = new HttpConfig();
// 加载代理设置
switch (ConfigManager.proxySetup()) {
case NO_PROXY -> httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
case MANUAL -> {
if (ConfigManager.hasProxy()) {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(
Objects.requireNonNull(ConfigManager.proxyHost()),
ConfigManager.getProxyPort()
);
httpConfig.setProxySelector(ProxySelector.of(unresolved));
}
}
}
httpConfig.setConnectTimeout(3000);
HttpUtil.init(httpConfig);
}
@Override
public void start(Stage primaryStage) throws IOException {
logger.info("application start ...");
Application.primaryStage = primaryStage;
Context.setApplication(this);
// 初始化弹窗工具
AlertUtil.initOwner(primaryStage);
// 全局异常处理
Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
// i18n
Context.setLanguage(ConfigManager.language());
// 主题样式
Application.setUserAgentStylesheet(ConfigManager.theme().getUserAgentStylesheet());
// 启动主界面
primaryStage.getIcons().add(new Image(Objects.requireNonNull(this.getClass().getResourceAsStream("/assets/logo.png"))));
primaryStage.initStyle(StageStyle.TRANSPARENT);
primaryStage.setTitle(String.format("%s %s", Constants.APP_TITLE, Constants.APP_VERSION));
Scene scene = Context.initScene();
primaryStage.setScene(scene);
primaryStage.show();
// 静默启动
if (ConfigManager.silentStartup()) {
Platform.setImplicitExit(false);
primaryStage.hide();
SystemTrayManager.show();
}
logger.info("application start over ...");
}
private void showErrorDialog(Thread t, Throwable e) {
logger.error("", e);
Platform.runLater(() -> AlertUtil.exception(new Exception(e)).show());
}
@Override
public void stop() {
logger.info("application stop ...");
// 关闭所有命令
ProcessesUtil.destroyAll();
// 保存应用数据
ConfigManager.save();
// 停止所有线程
ThreadPoolManager.getInstance().shutdown();
// 关闭主界面
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 <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) {
logger.error(STR."====\{instanceId}====");
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

@ -0,0 +1,57 @@
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<T extends BaseBuilder<T, ?>, D extends Dialog<?>> {
protected D dialog;
public BaseBuilder(D dialog, Window mOwner) {
this.dialog = dialog;
if (mOwner != null)
this.dialog.initOwner(mOwner);
}
public T title(String title) {
dialog.setTitle(title);
return (T) this;
}
public T header(String header) {
dialog.setHeaderText(header);
return (T) this;
}
public T content(String content) {
dialog.setContentText(content);
return (T) this;
}
public void show() {
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() {
if (dialog.isShowing())
dialog.close();
}
}

View File

@ -0,0 +1,160 @@
package cn.octopusyan.alistgui.base;
import cn.octopusyan.alistgui.Application;
import cn.octopusyan.alistgui.config.Context;
import cn.octopusyan.alistgui.config.I18n;
import cn.octopusyan.alistgui.util.FxmlUtil;
import cn.octopusyan.alistgui.util.ViewUtil;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Labeled;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
/**
* 通用视图控制器基类
*
* @author octopus_yan@foxmail.com
*/
public abstract class BaseController<VM extends BaseViewModel> 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<VM> clazz = (Class<VM>) type.getActualTypeArguments()[0];
try {
vm = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
logger.error("", e);
}
}
viewModel = vm;
}
/**
* 国际化绑定
*/
private void bindI18n() {
// i18n 绑定
try {
for (Field field : getAllField(this.getClass())) {
I18n i18n = field.getAnnotation(I18n.class);
if (i18n != null && StringUtils.isNoneEmpty(i18n.key())) {
switch (field.get(this)) {
case Labeled labeled -> labeled.textProperty().bind(Context.getLanguageBinding(i18n.key()));
case Tab tab -> tab.textProperty().bind(Context.getLanguageBinding(i18n.key()));
case MenuItem mi -> mi.textProperty().bind(Context.getLanguageBinding(i18n.key()));
default -> {
}
}
}
}
} catch (IllegalAccessException e) {
logger.error("获取属性失败", e);
}
}
@FXML
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// 全局窗口拖拽
if (dragWindow() && getRootPanel() != null) {
// 窗口拖拽
ViewUtil.bindDragged(getRootPanel());
}
// 国际化绑定
bindI18n();
// 初始化数据
initData();
// 初始化视图样式
initViewStyle();
// 初始化视图事件
initViewAction();
}
/**
* 窗口拖拽设置
*
* @return 是否启用
*/
public boolean dragWindow() {
return false;
}
/**
* 获取根布局
*
* @return 根布局对象
*/
public abstract Pane getRootPanel();
/**
* 获取根布局
* <p> 搭配 {@link FxmlUtil#load(String)} 使用
*
* @return 根布局对象
*/
protected String getRootFxml() {
System.out.println(getClass().getSimpleName());
return "";
}
protected Stage getWindow() {
return Application.getPrimaryStage();
}
/**
* 初始化数据
*/
public abstract void initData();
/**
* 视图样式
*/
public abstract void initViewStyle();
/**
* 视图事件
*/
public abstract void initViewAction();
private static List<Field> getAllField(Class<?> class1) {
List<Field> list = new ArrayList<>();
while (class1 != Object.class) {
list.addAll(Arrays.stream(class1.getDeclaredFields()).toList());
//获取父类
class1 = class1.getSuperclass();
}
return list;
}
}

View File

@ -0,0 +1,58 @@
package cn.octopusyan.alistgui.base;
import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager;
import javafx.concurrent.Task;
import lombok.Getter;
/**
* @author octopus_yan
*/
public abstract class BaseTask extends Task<Void> {
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 {
if (listener != null) listener.onStart();
task();
return null;
}
protected abstract void task() throws Exception;
public void onListen(Listener listener) {
this.listener = listener;
if (this.listener == null) return;
setOnRunning(_ -> listener.onRunning());
setOnCancelled(_ -> listener.onCancelled());
setOnFailed(_ -> listener.onFailed(getException()));
setOnSucceeded(_ -> listener.onSucceeded());
}
public void execute() {
Executor.execute(this);
}
public interface Listener {
default void onStart() {
}
default void onRunning() {
}
default void onCancelled() {
}
default void onFailed(Throwable throwable) {
}
void onSucceeded();
}
}

View File

@ -0,0 +1,13 @@
package cn.octopusyan.alistgui.base;
import lombok.Data;
/**
* View Model
*
* @author octopus_yan
*/
@Data
public abstract class BaseViewModel {
}

View File

@ -0,0 +1,28 @@
package cn.octopusyan.alistgui.config;
import cn.octopusyan.alistgui.util.PropertiesUtils;
import java.io.File;
import java.nio.file.Paths;
/**
* 应用信息
*
* @author octopus_yan@foxmail.com
*/
public class Constants {
public static final String APP_TITLE = PropertiesUtils.getInstance().getProperty("app.title");
public static final String APP_NAME = PropertiesUtils.getInstance().getProperty("app.name");
public static final String APP_VERSION = PropertiesUtils.getInstance().getProperty("app.version");
public static final String DATA_DIR_PATH = Paths.get("").toFile().getAbsolutePath();
public static final String BIN_DIR_PATH = STR."\{DATA_DIR_PATH}\{File.separator}bin";
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 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";
public static final String APP_EXE = STR."\{DATA_DIR_PATH}\{File.separator}\{APP_NAME}.exe";
}

View File

@ -0,0 +1,205 @@
package cn.octopusyan.alistgui.config;
import cn.octopusyan.alistgui.Application;
import cn.octopusyan.alistgui.base.BaseController;
import cn.octopusyan.alistgui.controller.*;
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.beans.binding.StringBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.URL;
import java.util.*;
/**
* test contect
*
* @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 currentViewIndex = new SimpleIntegerProperty(0);
/**
* 控制器集合
*/
@Getter
private static final Map<String, BaseController<?>> controllers = new HashMap<>();
/**
* 默认语言文件 Base Name
*/
private static final String LANGUAGE_RESOURCE_NAME = "language/language";
/**
* 语言资源工厂
*/
private static final ObservableResourceBundleFactory LANGUAGE_RESOURCE_FACTORY = new ObservableResourceBundleFactory();
/**
* 支持的语言集合,应与语言资源文件同步手动更新
*/
public static final List<Locale> SUPPORT_LANGUAGE_LIST = Arrays.asList(Locale.SIMPLIFIED_CHINESE, Locale.ENGLISH);
/**
* 记录当前所选时区
*/
private static final ObjectProperty<Locale> currentLocale = new SimpleObjectProperty<>();
private Context() {
throw new IllegalStateException("Utility class");
}
// 获取控制工厂
public static Callback<Class<?>, Object> getControlFactory() {
return type -> {
try {
return switch (type.getDeclaredConstructor().newInstance()) {
case RootController root -> root;
case MainController main -> main;
case SetupController setup -> setup;
case AboutController about -> about;
case PasswordController passwod -> passwod;
default -> throw new IllegalStateException(STR."Unexpected value: \{type}");
};
} catch (Exception e) {
log.error("", e);
return null;
}
};
}
public static void setApplication(Application application) {
Context.application = application;
}
// 获取当前所选时区属性
public static ObjectProperty<Locale> currentLocaleProperty() {
return currentLocale;
}
// 设置当前所选时区
public static void setCurrentLocale(Locale locale) {
currentLocaleProperty().set(locale);
}
/**
* 更换语言的组件使用此方法初始化自己的值,调用 {@link Context#setLanguage(Locale)} 来更新界面语言
*
* @return 当前界面语言
*/
// 获取当前界面语言
public static Locale getCurrentLocale() {
return currentLocaleProperty().get();
}
/**
* 更新界面语言
*
* @param locale 区域
*/
// 更新界面语言
public static void setLanguage(Locale locale) {
setCurrentLocale(locale);
Locale.setDefault(locale);
ConfigManager.language(locale);
LANGUAGE_RESOURCE_FACTORY.setResourceBundle(ResourceBundle.getBundle(LANGUAGE_RESOURCE_NAME, locale));
log.info("language changed to {}", locale);
ConsoleLog.info(STR."language changed to \{locale}");
}
/**
* 获取指定标识的字符串绑定
*
* @param key 标识
* @return 对应该标识的字符串属性绑定
*/
// 获取指定标识的字符串绑定
public static StringBinding getLanguageBinding(String key) {
return LANGUAGE_RESOURCE_FACTORY.getStringBinding(key);
}
/**
* 获取语言资源属性
*/
public static ObjectProperty<ResourceBundle> getLanguageResource() {
return LANGUAGE_RESOURCE_FACTORY.getResourceBundleProperty();
}
/**
* 有此类所在路径决定相对路径
*
* @param path 资源文件相对路径
* @return 资源文件路径
*/
// 加载资源文件
public static URL load(String path) {
return Context.class.getResource(path);
}
/**
* 初始化场景
*
* @return Scene
*/
public static Scene initScene() {
try {
FXMLLoader loader = FxmlUtil.load("root-view");
//底层面板
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);
}
return scene;
}
public static int currentViewIndex() {
return currentViewIndex.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()}");
}
}
}

View File

@ -0,0 +1,15 @@
package cn.octopusyan.alistgui.config;
import java.lang.annotation.*;
/**
* 显示文本绑定
*
* @author octopus_yan
*/
@Documented
@Target({ElementType.FIELD})//用此注解用在属性上。
@Retention(RetentionPolicy.RUNTIME)
public @interface I18n {
String key() default "";
}

View File

@ -0,0 +1,32 @@
package cn.octopusyan.alistgui.config;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Getter;
import java.util.ResourceBundle;
/**
* 多国语言属性绑定
*
* @author octopus_yan
*/
@Getter
public class ObservableResourceBundleFactory {
private final ObjectProperty<ResourceBundle> resourceBundleProperty = new SimpleObjectProperty<>();
public ResourceBundle getResourceBundle() {
return getResourceBundleProperty().get();
}
public void setResourceBundle(ResourceBundle resourceBundle) {
getResourceBundleProperty().set(resourceBundle);
}
public StringBinding getStringBinding(String key) {
return Bindings.createStringBinding(() -> getResourceBundle().getString(key), resourceBundleProperty);
}
}

View File

@ -0,0 +1,60 @@
package cn.octopusyan.alistgui.controller;
import cn.octopusyan.alistgui.base.BaseController;
import cn.octopusyan.alistgui.manager.ConfigManager;
import cn.octopusyan.alistgui.viewModel.AboutViewModule;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 关于
*
* @author octopus_yan
*/
public class AboutController extends BaseController<AboutViewModule> {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
public VBox aboutView;
public Label aListVersion;
public Label aListVersionLabel;
public Label appVersionLabel;
public Button checkAppVersion;
public Button checkAListVersion;
@Override
public VBox getRootPanel() {
return aboutView;
}
@Override
public void initData() {
}
@Override
public void initViewStyle() {
}
@Override
public void initViewAction() {
aListVersion.textProperty().bindBidirectional(viewModel.aListVersionProperty());
}
public void checkAListUpdate() {
viewModel.checkUpdate(ConfigManager.aList());
}
public void checkGuiUpdate() {
viewModel.checkUpdate(ConfigManager.gui());
}
}

View File

@ -0,0 +1,134 @@
package cn.octopusyan.alistgui.controller;
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 cn.octopusyan.alistgui.util.FxmlUtil;
import cn.octopusyan.alistgui.viewModel.MainViewModel;
import javafx.application.Platform;
import javafx.beans.binding.StringBinding;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
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<MainViewModel> {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
public VBox mainView;
public VBox logArea;
public ScrollPane logAreaSp;
public Button statusLabel;
public Button startButton;
public Button passwordButton;
public Button restartButton;
public MenuButton moreButton;
public MenuItem browserButton;
public MenuItem configButton;
public MenuItem logButton;
private PasswordController controller;
@Override
public VBox getRootPanel() {
return mainView;
}
@Override
public void initData() {
ConsoleLog.init(logAreaSp, logArea);
}
@Override
public void initViewStyle() {
// 运行状态监听
viewModel.runningProperty().addListener((_, _, running) -> {
resetStatus(running);
browserButton.disableProperty().set(!running);
});
}
@Override
public void initViewAction() {
}
// start button
public void start() {
if (AListManager.isRunning()) {
AListManager.stop();
} else {
AListManager.start();
}
}
// password button
public void adminPassword() throws IOException {
if (controller == null) {
FXMLLoader load = FxmlUtil.load("admin-panel");
load.load();
controller = load.getController();
}
controller.show();
}
// restart button
public void restart() {
AListManager.restart();
}
// more button
public void openInBrowser() {
AListManager.openScheme();
}
public void openLogFolder() {
AListManager.openLogFolder();
}
public void openConfig() {
AListManager.openConfig();
}
/**
* 根据运行状态改变按钮样式
*
* @param running 运行状态
*/
private void resetStatus(boolean running) {
String removeStyle = running ? "success" : "danger";
String addStyle = running ? "danger" : "success";
StringBinding button = Context.getLanguageBinding(STR."main.control.\{running ? "stop" : "start"}");
StringBinding status = Context.getLanguageBinding(STR."main.status.label-\{running ? "running" : "stop"}");
Platform.runLater(() -> {
startButton.getStyleClass().remove(removeStyle);
startButton.getStyleClass().add(addStyle);
startButton.textProperty().bind(button);
statusLabel.getStyleClass().remove(addStyle);
statusLabel.getStyleClass().add(removeStyle);
statusLabel.textProperty().bind(status);
});
}
}

View File

@ -0,0 +1,125 @@
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.Label;
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<AdminPanelViewModel> {
public AnchorPane adminPanel;
public Label toptip;
public Label usernameLabel;
public TextField usernameField;
@FXML
public Button copyUsername;
public Label passwordLabel;
public PasswordField passwordField;
public Button refreshPassword;
public Button savePassword;
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<Boolean> 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);
}
}

View File

@ -0,0 +1,154 @@
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.ViewUtil;
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.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
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<RootViewModel> {
// 布局
public StackPane rootPane;
public HBox windowHeader;
public FontIcon alwaysOnTopIcon;
public FontIcon minimizeIcon;
public FontIcon closeIcon;
// 界面
public TabPane tabPane;
public Tab mainTab;
public Tab setupTab;
public Tab aboutTab;
// footer
public Button document;
public Button github;
public Button sponsor;
private final ModalPane modalPane = new ModalPane();
/**
* 获取根布局
*
* @return 根布局对象
*/
@Override
public StackPane getRootPanel() {
return rootPane;
}
/**
* 初始化数据
*/
@Override
public void initData() {
tabPane.getSelectionModel().select(viewModel.currentViewIndexProperty().get());
}
/**
* 视图样式
*/
@Override
public void initViewStyle() {
// 设置图标
EmojiData.emojiFromShortName("book").ifPresent(icon -> {
ImageView book = EmojiImageUtils.emojiView(icon, 25);
document.setGraphic(book);
});
EmojiData.emojiFromShortName("cat").ifPresent(icon -> {
ImageView githubIcon = EmojiImageUtils.emojiView(icon, 25);
github.setGraphic(githubIcon);
});
EmojiData.emojiFromShortName("tropical_drink").ifPresent(icon -> {
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);
}
});
}
/**
* 视图事件
*/
@Override
public void initViewAction() {
closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> {
Platform.setImplicitExit(!ConfigManager.closeToTray());
if (ConfigManager.closeToTray()) {
SystemTrayManager.show();
} else {
SystemTrayManager.hide();
Platform.exit();
}
getWindow().close();
});
minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> getWindow().setIconified(true));
alwaysOnTopIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, _ -> {
boolean newVal = !getWindow().isAlwaysOnTop();
alwaysOnTopIcon.pseudoClassStateChanged(PseudoClass.getPseudoClass("always-on-top"), newVal);
getWindow().setAlwaysOnTop(newVal);
});
ViewUtil.bindDragged(windowHeader);
viewModel.currentViewIndexProperty().bind(tabPane.getSelectionModel().selectedIndexProperty());
}
public void openDocument() {
String locale = Context.getCurrentLocale().equals(Locale.ENGLISH) ? "" : "zh/";
Context.openUrl(STR."https://alist.nn.ci/\{locale}");
}
public void openGithub() {
Context.openUrl("https://github.com/alist-org/alist");
}
public void showTab(int index) {
if (index < 0 || index > 2) return;
tabPane.getSelectionModel().select(index);
}
public void showModal(Node node, boolean persistent) {
modalPane.show(node);
modalPane.setPersistent(persistent);
}
public void hideModal() {
modalPane.hide(false);
modalPane.setPersistent(false);
}
}

View File

@ -0,0 +1,113 @@
package cn.octopusyan.alistgui.controller;
import atlantafx.base.theme.Theme;
import cn.octopusyan.alistgui.base.BaseController;
import cn.octopusyan.alistgui.config.Context;
import cn.octopusyan.alistgui.enums.ProxySetup;
import cn.octopusyan.alistgui.manager.ConfigManager;
import cn.octopusyan.alistgui.view.ProxySetupCell;
import cn.octopusyan.alistgui.viewModel.SetupViewModel;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Locale;
/**
* 设置页面控制器
*
* @author octopus_yan
*/
public class SetupController extends BaseController<SetupViewModel> implements Initializable {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@FXML
public VBox setupView;
public CheckBox autoStartCheckBox;
public CheckBox silentStartupCheckBox;
public CheckBox closeToTrayCheckBox;
public Label themeLabel;
public ComboBox<Theme> themeComboBox;
public Label languageLabel;
public ComboBox<Locale> languageComboBox;
public Label proxySetupLabel;
public ComboBox<ProxySetup> proxySetupComboBox;
public Pane proxySetupPane;
public Button proxyCheck;
public TextField proxyHost;
public TextField proxyPort;
public Label hostLabel;
public Label portLabel;
@Override
public VBox getRootPanel() {
return setupView;
}
@Override
public void initData() {
languageComboBox.setItems(FXCollections.observableList(Context.SUPPORT_LANGUAGE_LIST));
themeComboBox.setItems(FXCollections.observableList(ConfigManager.THEME_LIST));
proxySetupComboBox.setItems(FXCollections.observableList(List.of(ProxySetup.values())));
proxySetupComboBox.setCellFactory(_ -> new ProxySetupCell());
proxySetupComboBox.setButtonCell(new ProxySetupCell());
themeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(Theme object) {
return object.getName();
}
@Override
public Theme fromString(String string) {
return ConfigManager.THEME_MAP.get(string);
}
});
}
@Override
public void initViewStyle() {
proxySetupComboBox.getSelectionModel().selectedItemProperty().addListener((_, _, newValue) -> {
proxySetupPane.setVisible(ProxySetup.MANUAL.equals(newValue));
proxyCheck.setVisible(!ProxySetup.NO_PROXY.equals(newValue));
// proxySetupComboBox.promptTextProperty().bind(
//// Bindings.createStringBinding(
//// () -> Context.getLanguageBinding(STR."proxy.setup.label.\{newValue.getName()}").get(),
//// Context.currentLocaleProperty()
//// )
// Context.getLanguageBinding(STR."proxy.setup.label.\{newValue.getName()}")
// );
});
languageComboBox.getSelectionModel().select(ConfigManager.language());
themeComboBox.getSelectionModel().select(ConfigManager.theme());
proxySetupComboBox.getSelectionModel().select(ConfigManager.proxySetup());
}
@Override
public void initViewAction() {
//
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());
viewModel.languageProperty().bind(languageComboBox.getSelectionModel().selectedItemProperty());
viewModel.themeProperty().bind(themeComboBox.getSelectionModel().selectedItemProperty());
viewModel.proxySetupProperty().bind(proxySetupComboBox.getSelectionModel().selectedItemProperty());
}
public void proxyTest() {
viewModel.proxyTest();
}
}

View File

@ -0,0 +1,30 @@
package cn.octopusyan.alistgui.enums;
import cn.octopusyan.alistgui.config.Context;
import javafx.beans.binding.StringBinding;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 代理类型
*
* @author octopus_yan
*/
@Getter
@RequiredArgsConstructor
public enum ProxySetup {
NO_PROXY("no_proxy"),
SYSTEM("system"),
MANUAL("manual");
private final String name;
@Override
public String toString() {
return getBinding().get();
}
public StringBinding getBinding() {
return Context.getLanguageBinding(STR."proxy.setup.label.\{getName()}");
}
}

View File

@ -0,0 +1,243 @@
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() {
return running;
}
public static boolean isRunning() {
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() {
if (!checkAList()) return;
if (running.get() || util.isRunning()) {
ConsoleLog.warning(getText("alist.status.start.running"));
return;
}
ConsoleLog.info(getText("alist.status.start"));
loadConfig();
util.exec(START_COMMAND, runningListener);
}
public static void stop() {
ConsoleLog.info(getText("alist.status.stop"));
if (!running.get()) {
ConsoleLog.warning(getText("alist.status.stop.stopped"));
return;
}
util.destroy();
}
static ChangeListener<Boolean> restartListener;
public static void restart() {
if (!running.get()) {
start();
} else {
stop();
restartListener = (_, _, run) -> {
if (run) return;
running.removeListener(restartListener);
start();
};
running.addListener(restartListener);
}
}
public static void resetPassword() {
resetPassword("");
}
static ChangeListener<Boolean> 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(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.UpgradeListener(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));
}
}

View File

@ -0,0 +1,291 @@
package cn.octopusyan.alistgui.manager;
import atlantafx.base.theme.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.PatternPool;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.NumberUtil;
import cn.octopusyan.alistgui.Application;
import cn.octopusyan.alistgui.config.Constants;
import cn.octopusyan.alistgui.enums.ProxySetup;
import cn.octopusyan.alistgui.manager.http.HttpUtil;
import cn.octopusyan.alistgui.manager.thread.ThreadPoolManager;
import cn.octopusyan.alistgui.model.GuiConfig;
import cn.octopusyan.alistgui.model.ProxyInfo;
import cn.octopusyan.alistgui.model.UpgradeConfig;
import cn.octopusyan.alistgui.model.upgrade.AList;
import cn.octopusyan.alistgui.model.upgrade.Gui;
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;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
/**
* 客户端设置
*
* @author octopus_yan@foxmail.com
*/
public class ConfigManager {
private static final Logger logger = LoggerFactory.getLogger(ConfigManager.class);
public static ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
public static final Locale DEFAULT_LANGUAGE = Locale.SIMPLIFIED_CHINESE;
public static final String DEFAULT_THEME = new PrimerLight().getName();
public static List<Theme> THEME_LIST = List.of(
new PrimerLight(), new PrimerDark(),
new NordLight(), new NordDark(),
new CupertinoLight(), new CupertinoDark(),
new Dracula()
);
public static Map<String, Theme> THEME_MAP = THEME_LIST.stream()
.collect(Collectors.toMap(Theme::getName, Function.identity()));
private static GuiConfig guiConfig;
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
}
public static void load() {
guiConfig = loadConfig(Constants.GUI_CONFIG_PATH, GuiConfig.class);
}
public static <T> T loadConfig(String path, Class<T> clazz) {
File src = new File(path);
try {
if (!src.exists()) {
checkFile(src, clazz);
}
return objectMapper.readValue(src, clazz);
} catch (Exception e) {
logger.error(String.format("load %s error", clazz.getSimpleName()), e);
}
return null;
}
private static <T> void checkFile(File src, Class<T> clazz) throws Exception {
File parent = FileUtil.getParent(src, 1);
if (!parent.exists()) {
boolean wasSuccessful = parent.mkdirs();
if (!wasSuccessful)
logger.error("{} 创建失败", src.getAbsolutePath());
}
objectMapper.writeValue(src, clazz.getDeclaredConstructor().newInstance());
}
public static void save() {
try {
objectMapper.writeValue(new File(Constants.GUI_CONFIG_PATH), guiConfig);
} catch (IOException e) {
logger.error("save config error", e);
}
}
// --------------------------------{ 主题 }------------------------------------------
public static String themeName() {
return guiConfig.getTheme();
}
public static Theme theme() {
return THEME_MAP.get(themeName());
}
public static void theme(Theme theme) {
Application.setUserAgentStylesheet(theme.getUserAgentStylesheet());
guiConfig.setTheme(theme.getName());
}
// --------------------------------{ 网络代理 }------------------------------------------
public static ProxySetup proxySetup() {
return ProxySetup.valueOf(StringUtils.upperCase(guiConfig.getProxySetup()));
}
public static void proxyTestUrl(String url) {
guiConfig.setProxyTestUrl(url);
}
public static String proxyTestUrl() {
return guiConfig.getProxyTestUrl();
}
public static void proxySetup(ProxySetup setup) {
guiConfig.setProxySetup(setup.getName());
switch (setup) {
case NO_PROXY -> HttpUtil.getInstance().clearProxy();
case SYSTEM, MANUAL -> {
if (ProxySetup.MANUAL.equals(setup) && !hasProxy())
return;
HttpUtil.getInstance().proxy(setup, ConfigManager.getProxyInfo());
}
}
}
public static boolean hasProxy() {
if (guiConfig == null)
return false;
ProxyInfo proxyInfo = getProxyInfo();
return proxyInfo != null
&& StringUtils.isNoneEmpty(proxyInfo.getHost())
&& StringUtils.isNoneEmpty(proxyInfo.getPort())
&& Integer.parseInt(proxyInfo.getPort()) > 0;
}
public static ProxyInfo getProxyInfo() {
ProxyInfo proxyInfo = guiConfig.getProxyInfo();
if (proxyInfo == null)
setProxyInfo(new ProxyInfo());
return guiConfig.getProxyInfo();
}
private static void setProxyInfo(ProxyInfo info) {
guiConfig.setProxyInfo(info);
}
public static String proxyHost() {
return getProxyInfo().getHost();
}
public static void proxyHost(String host) {
final Matcher matcher = PatternPool.IPV4.matcher(host);
if (!matcher.matches()) return;
getProxyInfo().setHost(host);
}
public static String proxyPort() {
return getProxyInfo().getPort();
}
public static int getProxyPort() {
return Integer.parseInt(proxyPort());
}
public static void proxyPort(String port) {
if (!NumberUtil.isNumber(port)) return;
getProxyInfo().setPort(port);
}
public static void checkProxy(BiConsumer<Boolean, String> consumer) {
if (ProxySetup.SYSTEM.equals(proxySetup())) {
consumer.accept(true, "");
return;
}
if (!hasProxy()) return;
ThreadPoolManager.getInstance().execute(() -> {
try {
InetSocketAddress address = NetUtil.createAddress(proxyHost(), getProxyPort());
if (NetUtil.isOpen(address, 1000)) {
Platform.runLater(() -> consumer.accept(true, "success"));
} else {
Platform.runLater(() -> consumer.accept(false, "connection timed out"));
}
} catch (Exception e) {
logger.error(STR."host=\{proxyHost()},port=\{proxyPort()}", e);
Platform.runLater(() -> consumer.accept(false, e.getMessage()));
}
});
}
// --------------------------------{ 语言 }------------------------------------------
public static Locale language() {
String language = guiConfig.getLanguage();
return LocaleUtils.toLocale(Optional.ofNullable(language).orElse(DEFAULT_LANGUAGE.toString()));
}
public static void language(Locale locale) {
guiConfig.setLanguage(locale.toString());
}
// --------------------------------{ 开机自启 }------------------------------------------
public static boolean autoStart() {
return guiConfig.getAutoStart();
}
public static void autoStart(Boolean autoStart) {
guiConfig.setAutoStart(autoStart);
}
// --------------------------------{ 静默启动 }------------------------------------------
public static boolean silentStartup() {
return guiConfig.getSilentStartup();
}
public static void silentStartup(Boolean startup) {
guiConfig.setSilentStartup(startup);
}
// --------------------------------{ 最小化到托盘 }------------------------------------------
public static boolean closeToTray() {
return guiConfig.getCloseToTray();
}
public static void closeToTray(boolean check) {
guiConfig.setCloseToTray(check);
}
// --------------------------------{ 版本检查 }------------------------------------------
public static UpgradeConfig upgradeConfig() {
return guiConfig.getUpgradeConfig();
}
public static AList aList() {
return upgradeConfig().getAList();
}
public static String aListVersion() {
return aList().getVersion();
}
public static StringProperty aListVersionProperty() {
return aList().versionProperty();
}
public static void aListVersion(String version) {
aListVersionProperty().set(version);
}
public static Gui gui() {
return upgradeConfig().getGui();
}
public static String guiVersion() {
// 覆盖配置文件读取的版本号
if (!Constants.APP_VERSION.equals(gui().getVersion())) {
guiVersion(Constants.APP_VERSION);
}
return gui().getVersion();
}
public static void guiVersion(String version) {
gui().setVersion(version);
}
}

View File

@ -0,0 +1,256 @@
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.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;
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;
/**
* 模拟控制台输出
*
* @author octopus_yan
*/
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[";
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;
textArea.heightProperty().subscribe(() -> logAreaSp.vvalueProperty().setValue(1));
}
public static ConsoleLog getInstance() {
if (log == null) {
throw new RuntimeException("are you ready ?");
}
return log;
}
public static void init(ScrollPane logAreaSp, VBox textArea) {
synchronized (ConsoleLog.class) {
log = new ConsoleLog(logAreaSp, textArea);
}
}
public static boolean isInit() {
return log != null;
}
public static void info(String message, Object... param) {
info("", message, param);
}
public static void warning(String message, Object... param) {
warning("", message, param);
}
public static void error(String message, Object... param) {
error("", message, param);
}
public static void info(String tag, String message, Object... param) {
printLog(tag, Level.INFO, message, param);
}
public static void warning(String tag, String message, Object... param) {
printLog(tag, Level.WARN, message, param);
}
public static void error(String tag, String message, Object... param) {
printLog(tag, Level.ERROR, message, param);
}
public static void msg(String message, Object... param) {
if (StringUtils.isEmpty(message) || !isInit()) return;
message = message.strip();
message = StrUtil.format(message, param);
// 多颜色消息处理
if (StringUtils.countMatches(message, CONSOLE_COLOR_PREFIX) > 1) {
String[] split = message.replace(CONSOLE_MSG_REX, "\n%s".formatted(CONSOLE_COLOR_PREFIX)).split("\n");
List<String> 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]";
// 拼接后输出
String input = STR."\{time} \{levelStr} - \{tag}\{message}";
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") ? text : STR."http://\{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 -> {
}
}
});
}
/**
* 控制台输出颜色
*
* @param msg alist 输出消息
* @return bbcode 颜色文本
*/
private static String resetConsoleColor(String msg) {
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;
}
/**
* @param level 级别
* @return bbcode 颜色
*/
private static String resetLevelColor(Level level) {
return color(level.getColor(), level.getCode());
}
private static String color(String color, String msg) {
String PREFIX = STR."\{StringUtils.isEmpty(color) ? "" : STR."[color=\{color}]"}";
String SUFFIX = STR."\{StringUtils.isEmpty(color) ? "" : "[/color]"}";
return STR."\{PREFIX}\{msg}\{SUFFIX}";
}
//============================{ 枚举 }================================
@Getter
@RequiredArgsConstructor
public enum Level {
INFO("INFO", null),
WARN("WARN", "-color-danger-emphasis"),
ERROR("ERROR", "-color-danger-fg"),
;
private final String code;
private final String color;
}
@RequiredArgsConstructor
@Getter
enum Color {
BLACK(30, "-color-fg-default"),
RED(31, "-color-danger-fg"),
GREEN(32, "-color-success-fg"),
YELLOW(33, "-color-warning-emphasis"),
BLUE(34, "-color-accent-fg"),
PINKISH_RED(35, "-color-danger-4"),
CYAN(36, "-color-accent-emphasis"),
WHITE(37, "-color-bg-default");
private final int code;
private final String color;
public static final Map<String, Color> NAME_CODE = Arrays.stream(Color.values())
.collect(Collectors.toMap(Color::name, Function.identity()));
public static final Map<Integer, Color> CODE_NAME = Arrays.stream(Color.values())
.collect(Collectors.toMap(Color::getCode, Function.identity()));
public static Color valueOf(int code) {
return CODE_NAME.get(code);
}
}
}

View File

@ -0,0 +1,192 @@
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.ViewUtil;
import cn.octopusyan.alistgui.view.PopupMenu;
import javafx.application.Platform;
import javafx.beans.binding.StringBinding;
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(ViewUtil.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(AListManager.isRunning());
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(boolean running) {
if (trayIcon != null) return;
// 系统托盘图标
URL resource = ViewUtil.class.getResource(STR."/assets/logo\{running ? "" : "-disabled"}.png");
Image image = Toolkit.getDefaultToolkit().getImage(resource);
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(running);
popupMenu.show(event);
});
} else if (event.getButton() == MouseEvent.BUTTON1) {
// 显示 PrimaryStage
Platform.runLater(() -> Application.getPrimaryStage().show());
}
}
});
}
/**
* 构建托盘菜单
*/
private static void initPopupMenu(boolean running) {
if (popupMenu != null) return;
MenuItem start = PopupMenu.menuItem(
getStringBinding(STR."main.control.\{running ? "stop" : "start"}"),
_ -> AListManager.openScheme()
);
MenuItem browser = PopupMenu.menuItem(getStringBinding("main.more.browser"), _ -> AListManager.openScheme());
browser.setDisable(!running);
AListManager.runningProperty().addListener((_, _, newValue) -> {
start.textProperty().unbind();
start.textProperty().bind(getStringBinding(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(getStringBinding("main.control.restart"), _ -> AListManager.restart())
.addMenu(getStringBinding("main.control.more"), browser,
PopupMenu.menuItem(getStringBinding("main.more.open-config"), _ -> AListManager.openConfig()),
PopupMenu.menuItem(getStringBinding("main.more.open-log"), _ -> AListManager.openLogFolder()))
.addSeparator()
.addExitItem();
}
private static StringBinding getStringBinding(String key) {
return Context.getLanguageBinding(key);
}
private static Stage stage() {
return ViewUtil.getStage();
}
}

View File

@ -0,0 +1,114 @@
package cn.octopusyan.alistgui.manager.http;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.concurrent.Executor;
/**
* Http配置参数
*
* @author octopus_yan@foxmail.com
*/
@Data
public class HttpConfig {
private static final Logger logger = LoggerFactory.getLogger(HttpConfig.class);
static {
// 使用系统默认代理
System.setProperty("java.net.useSystemProxies", "true");
}
/**
* http版本
*/
private HttpClient.Version version = HttpClient.Version.HTTP_2;
/**
* 转发策略
*/
private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
/**
* 线程池
*/
private Executor executor;
/**
* 认证
*/
private Authenticator authenticator;
/**
* 代理
*/
private ProxySelector proxySelector;
/**
* CookieHandler
*/
private CookieHandler cookieHandler;
/**
* sslContext
*/
private SSLContext sslContext;
/**
* sslParams
*/
private SSLParameters sslParameters;
/**
* 连接超时时间毫秒
*/
private int connectTimeout = 10000;
/**
* 默认读取数据超时时间
*/
private int defaultReadTimeout = 1200000;
public HttpConfig() {
TrustManager[] trustAllCertificates = new X509TrustManager[]{new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; // Not relevant.
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
// TODO Auto-generated method stub
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
// TODO Auto-generated method stub
}
}};
sslParameters = new SSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("");
sslParameters.setProtocols(new String[]{"TLSv1.2"});
try {
sslContext = SSLContext.getInstance("TLSv1.2");
System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true");//取消主机名验证
sslContext.init(null, trustAllCertificates, new SecureRandom());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.error("", e);
}
}
}

View File

@ -0,0 +1,180 @@
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;
import java.net.URI;
import java.net.URLEncoder;
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
private volatile HttpClient httpClient;
private final HttpConfig httpConfig;
private HttpUtil(HttpConfig httpConfig) {
this.httpConfig = httpConfig;
this.httpClient = createClient(httpConfig);
}
public static HttpUtil getInstance() {
if (util == null) {
throw new RuntimeException("are you ready ?");
}
return util;
}
public static void init(HttpConfig httpConfig) {
synchronized (HttpUtil.class) {
util = new HttpUtil(httpConfig);
}
}
private static HttpClient createClient(HttpConfig httpConfig) {
HttpClient.Builder builder = HttpClient.newBuilder()
.version(httpConfig.getVersion())
.connectTimeout(Duration.ofMillis(httpConfig.getConnectTimeout()))
.sslContext(httpConfig.getSslContext())
.sslParameters(httpConfig.getSslParameters())
.followRedirects(httpConfig.getRedirect());
Optional.ofNullable(httpConfig.getAuthenticator()).ifPresent(builder::authenticator);
Optional.ofNullable(httpConfig.getCookieHandler()).ifPresent(builder::cookieHandler);
Optional.ofNullable(httpConfig.getProxySelector()).ifPresent(builder::proxy);
Optional.ofNullable(httpConfig.getExecutor()).ifPresent(builder::executor);
return builder.build();
}
public void proxy(ProxySetup setup, ProxyInfo proxy) {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
switch (setup) {
case NO_PROXY -> clearProxy();
case SYSTEM -> httpConfig.setProxySelector(ProxySelector.getDefault());
case MANUAL -> {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(proxy.getHost(), Integer.parseInt(proxy.getPort()));
httpConfig.setProxySelector(ProxySelector.of(unresolved));
}
}
this.httpClient = createClient(httpConfig);
}
public void clearProxy() {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
httpConfig.setProxySelector(HttpClient.Builder.NO_PROXY);
httpClient = createClient(httpConfig);
}
public String get(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header).GET();
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
public String post(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri, header)
.POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJsonString(param)));
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
public String postForm(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
.POST(HttpRequest.BodyPublishers.noBody());
HttpResponse<String> response = httpClient.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.body();
}
public void download(String url, String savePath, BiConsumer<Long, Long> 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<Path> response = httpClient.send(request, handler);
}
private HttpRequest.Builder getRequest(String uri, JsonNode header) {
HttpRequest.Builder request = HttpRequest.newBuilder();
// 请求地址
request.uri(URI.create(uri));
// 请求头
if (header != null && !header.isEmpty()) {
for (Map.Entry<String, JsonNode> property : header.properties()) {
String key = property.getKey();
request.header(key, JsonUtil.toJsonString(property.getValue()));
}
}
return request;
}
private String createFormParams(JsonNode params) {
StringBuilder formParams = new StringBuilder();
if (params == null) {
return formParams.toString();
}
for (Map.Entry<String, JsonNode> property : params.properties()) {
String key = property.getKey();
JsonNode value = params.get(key);
if (value.isTextual()) {
String value_ = URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8);
formParams.append("&").append(key).append("=").append(value_);
} else if (value.isNumber()) {
formParams.append("&").append(key).append("=").append(value);
} else if (value.isArray()) {
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
} else {
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
}
}
if (!formParams.isEmpty()) {
formParams = new StringBuilder(STR."?\{formParams.substring(1)}");
}
return formParams.toString();
}
}

View File

@ -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<Path> {
private final HttpResponse.BodyHandler<Path> handler;
private BiConsumer<Long, Long> consumer;
private BodyHandler(HttpResponse.BodyHandler<Path> handler) {
this.handler = handler;
}
public static BodyHandler create(Path directory, OpenOption... openOptions) {
return new BodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions));
}
@Override
public HttpResponse.BodySubscriber<Path> apply(HttpResponse.ResponseInfo responseInfo) {
AtomicLong length = new AtomicLong(-1);
// 获取文件大小
Optional<String> 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<Long, Long> consumer) {
this.consumer = consumer;
}
public static class BodySubscriber implements HttpResponse.BodySubscriber<Path> {
private final HttpResponse.BodySubscriber<Path> subscriber;
private final AtomicLong progress = new AtomicLong(0);
@Setter
private Consumer<Long> consumer;
public BodySubscriber(HttpResponse.BodySubscriber<Path> subscriber) {
this.subscriber = subscriber;
}
@Override
public CompletionStage<Path> getBody() {
return subscriber.getBody();
}
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscriber.onSubscribe(subscription);
}
@Override
public void onNext(List<ByteBuffer> 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());
}
}
}

View File

@ -0,0 +1,49 @@
package cn.octopusyan.alistgui.manager.thread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义线程工厂
*
* @author octopus_yan@foxmail.com
*/
public class ThreadFactory implements java.util.concurrent.ThreadFactory {
private static final Logger logger = LoggerFactory.getLogger(ThreadFactory.class);
public static final String DEFAULT_THREAD_PREFIX = "thread-factory-pool";
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public ThreadFactory() {
this(DEFAULT_THREAD_PREFIX);
}
public ThreadFactory(String prefix) {
group = Thread.currentThread().getThreadGroup();
namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-";
}
@Override
public Thread newThread(Runnable runnable) {
Thread t = new Thread(group, runnable,
namePrefix + threadNumber.getAndIncrement(),
0);
t.setUncaughtExceptionHandler((t1, e) -> {
logger.error("thread : {}, error", t1.getName(), e);
});
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}

View File

@ -0,0 +1,29 @@
package cn.octopusyan.alistgui.manager.thread;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池管理类
*/
public final class ThreadPoolManager extends ThreadPoolExecutor {
private static volatile ThreadPoolManager sInstance;
private ThreadPoolManager() {
super(32,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadFactory(ThreadFactory.DEFAULT_THREAD_PREFIX),
new DiscardPolicy());
}
public static ThreadPoolManager getInstance() {
if (sInstance == null) sInstance = new ThreadPoolManager();
return sInstance;
}
}

View File

@ -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<String> allowOrigins;
@JsonProperty("allow_methods")
private List<String> allowMethods;
@JsonProperty("allow_headers")
private List<String> allowHeaders;
}
@NoArgsConstructor
@Getter
public static class S3 {
@JsonProperty("enable")
private Boolean enable;
@JsonProperty("port")
private Integer port;
@JsonProperty("ssl")
private Boolean ssl;
}
}

View File

@ -0,0 +1,31 @@
package cn.octopusyan.alistgui.model;
import cn.octopusyan.alistgui.enums.ProxySetup;
import cn.octopusyan.alistgui.manager.ConfigManager;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* GUI配置信息
*
* @author octopus_yan
*/
@Data
public class GuiConfig {
private static final Logger log = LoggerFactory.getLogger(GuiConfig.class);
private Boolean autoStart = false;
private Boolean silentStartup = false;
private Boolean closeToTray = false;
@JsonProperty("proxy")
private ProxyInfo proxyInfo;
@JsonProperty("proxy.testUrl")
private String proxyTestUrl = "http://";
private String proxySetup = ProxySetup.NO_PROXY.getName();
private String language = ConfigManager.DEFAULT_LANGUAGE.toString();
private String theme = ConfigManager.DEFAULT_THEME;
@JsonProperty("upgrade")
private UpgradeConfig upgradeConfig = new UpgradeConfig();
}

View File

@ -0,0 +1,16 @@
package cn.octopusyan.alistgui.model;
import lombok.Data;
/**
* 代理信息
*
* @author octopus_yan
*/
@Data
public class ProxyInfo {
private String host = "";
private String port = "";
private String username = "";
private String password = "";
}

View File

@ -0,0 +1,16 @@
package cn.octopusyan.alistgui.model;
import cn.octopusyan.alistgui.model.upgrade.AList;
import cn.octopusyan.alistgui.model.upgrade.Gui;
import lombok.Data;
/**
* 更新配置
*
* @author octopus_yan
*/
@Data
public class UpgradeConfig {
private AList aList = new AList();
private Gui gui = new Gui();
}

View File

@ -0,0 +1,32 @@
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;
/**
* @author octopus_yan
*/
@Data
public class AList implements UpgradeApp {
@JsonIgnore
private final String owner = "alist-org";
@JsonIgnore
private final String repo = "alist";
private String releaseFile = "alist-windows-amd64.zip";
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();
}
}

View File

@ -0,0 +1,21 @@
package cn.octopusyan.alistgui.model.upgrade;
import cn.octopusyan.alistgui.util.PropertiesUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/**
* @author octopus_yan
*/
@Data
public class Gui implements UpgradeApp {
@JsonIgnore
private final String owner = "octopusYan";
@JsonIgnore
private final String repo = "alist-gui";
private String releaseFile = "alist-gui-windows-nojre.zip";
private String version = PropertiesUtils.getInstance().getProperty("app.version");
}

View File

@ -0,0 +1,26 @@
package cn.octopusyan.alistgui.model.upgrade;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author octopus_yan
*/
public interface UpgradeApp {
@JsonIgnore
default String getReleaseApi() {
return STR."https://api.github.com/repos/\{getOwner()}/\{getRepo()}/releases/latest";
}
@JsonIgnore
default String getDownloadUrl(String version) {
return STR."https://github.com/\{getOwner()}/\{getRepo()}/releases/download/\{version}/\{getReleaseFile()}";
}
String getOwner();
String getRepo();
String getReleaseFile();
String getVersion();
}

View File

@ -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);
}
}

View File

@ -0,0 +1,39 @@
package cn.octopusyan.alistgui.task;
import cn.octopusyan.alistgui.base.BaseTask;
import cn.octopusyan.alistgui.manager.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
/**
* TODO 下载任务
*
* @author octopus_yan
*/
@Slf4j
public class DownloadTask extends BaseTask {
private final String downloadUrl;
private final String savePath;
public DownloadTask(String downloadUrl, String savePath) {
super(STR."Download \{downloadUrl}");
this.downloadUrl = downloadUrl;
this.savePath = savePath;
}
public void onListen(DownloadListener listener) {
super.onListen(listener);
}
@Override
protected void task() throws Exception {
HttpUtil.getInstance().download(
downloadUrl,
savePath,
listener instanceof DownloadListener ? ((DownloadListener) listener)::onProgress : null
);
}
public interface DownloadListener extends BaseTask.Listener {
void onProgress(Long total, Long progress);
}
}

View File

@ -0,0 +1,27 @@
package cn.octopusyan.alistgui.task;
import cn.octopusyan.alistgui.base.BaseTask;
import cn.octopusyan.alistgui.manager.http.HttpUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 代理检测任务
*
* @author octopus_yan
*/
@Slf4j
public class ProxyCheckTask extends BaseTask {
private final String checkUrl;
public ProxyCheckTask(String checkUrl) {
super(STR."ProxyCheck[\{checkUrl}]");
this.checkUrl = checkUrl;
this.updateProgress(0d, 1d);
}
@Override
protected void task() throws Exception {
String response = HttpUtil.getInstance().get(checkUrl, null, null);
log.debug(STR."Proxy check response result => \n\{response}");
}
}

View File

@ -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 UpgradeListener extends TaskListener implements CheckUpdateTask.UpgradeListener {
public UpgradeListener(BaseTask task) {
super(task);
}
@Override
protected void onSucceed() {
// do nothing ...
}
}
}

View File

@ -0,0 +1,102 @@
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.config.Context;
import cn.octopusyan.alistgui.controller.RootController;
import cn.octopusyan.alistgui.manager.ConsoleLog;
import cn.octopusyan.alistgui.model.upgrade.AList;
import cn.octopusyan.alistgui.model.upgrade.Gui;
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 parentPath = switch (app) {
case AList _ -> Constants.BIN_DIR_PATH;
case Gui _ -> Constants.DATA_DIR_PATH;
default -> throw new IllegalStateException(STR."Unexpected value: \{app}");
};
var task = new DownloadTask(app.getDownloadUrl(version), parentPath);
task.onListen(new TaskListener.DownloadListener(task) {
@Override
public void onRunning() {
// 不展示进度条
RootController root = (RootController) Context.getControllers().get(RootController.class.getSimpleName());
root.showTab(0);
}
@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) {
unzip(app, true);
}
public static void unzip(UpgradeApp app, boolean del) {
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, "*", "_");
}
// 打包后文件都在alist-gui文件夹下解压时去掉
if (app instanceof Gui) {
path = path.replaceFirst(Constants.APP_NAME, "");
}
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()}");
}
});
// 解压完成后删除
if (del) FileUtil.del(file);
}
}

View File

@ -0,0 +1,32 @@
package cn.octopusyan.alistgui.util;
import cn.octopusyan.alistgui.config.Context;
import javafx.fxml.FXMLLoader;
import javafx.fxml.JavaFXBuilderFactory;
import java.nio.charset.StandardCharsets;
import java.util.ResourceBundle;
/**
* FXML 工具
*
* @author octopus_yan@foxmail.com
*/
public class FxmlUtil {
public static FXMLLoader load(String name) {
return load(name, Context.getLanguageResource().get());
}
public static FXMLLoader load(String name, ResourceBundle bundle) {
String prefix = "/fxml/";
String suffix = ".fxml";
return new FXMLLoader(
FxmlUtil.class.getResource(prefix + name + suffix),
bundle,
new JavaFXBuilderFactory(),
Context.getControlFactory(),
StandardCharsets.UTF_8
);
}
}

View File

@ -0,0 +1,187 @@
package cn.octopusyan.alistgui.util;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
/**
* Jackson 封装工具类
*
* @author octopus_yan
*/
public class JsonUtil {
private static final Logger log = LoggerFactory.getLogger(JsonUtil.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 时间日期格式
*/
private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
static {
//对象的所有字段全部列入序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
//取消默认转换timestamps形式
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//忽略空Bean转json的错误
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//所有的日期格式都统一为以下的格式即yyyy-MM-dd HH:mm:ss
objectMapper.setDateFormat(new SimpleDateFormat(STANDARD_FORMAT));
//忽略 在json字符串中存在但在java对象中不存在对应属性的情况。防止错误
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* Json字符串 转 JavaBean
*
* @param jsonString Json字符串
* @param clazz Java类对象
* @param <T> Java类
* @return JavaBean
*/
public static <T> T parseObject(String jsonString, Class<T> clazz) {
T t = null;
try {
t = objectMapper.readValue(jsonString, clazz);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return t;
}
/**
* 读取Json文件 转 JavaBean
*
* @param file Json文件
* @param clazz Java类对象
* @param <T> Java类
* @return JavaBean
*/
public static <T> T parseObject(File file, Class<T> clazz) {
T t = null;
try {
t = objectMapper.readValue(file, clazz);
} catch (IOException e) {
log.error("失败:{}", e.getMessage());
}
return t;
}
/**
* 读取Json字符串 转 JavaBean集合
*
* @param jsonArray Json字符串
* @param reference 类型
* @param <T> JavaBean类型
* @return JavaBean集合
*/
public static <T> T parseJsonArray(String jsonArray, TypeReference<T> reference) {
T t = null;
try {
t = objectMapper.readValue(jsonArray, reference);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return t;
}
/**
* JavaBean 转 Json字符串
*
* @param object JavaBean
* @return Json字符串
*/
public static String toJsonString(Object object) {
String jsonString = null;
try {
jsonString = objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return jsonString;
}
/**
* JavaBean 转 字节数组
*
* @param object JavaBean
* @return 字节数组
*/
public static byte[] toByteArray(Object object) {
byte[] bytes = null;
try {
bytes = objectMapper.writeValueAsBytes(object);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return bytes;
}
/**
* JavaBean序列化到文件
*
* @param file 写入文件对象
* @param object JavaBean
*/
public static void objectToFile(File file, Object object) {
try {
objectMapper.writeValue(file, object);
} catch (Exception e) {
log.error("失败:{}", e.getMessage());
}
}
/**
* Json字符串 转 JsonNode
*
* @param jsonString Json字符串
* @return JsonNode
*/
public static JsonNode parseJsonObject(String jsonString) {
JsonNode jsonNode = null;
try {
jsonNode = objectMapper.readTree(jsonString);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return jsonNode;
}
/**
* JavaBean 转 JsonNode
*
* @param object JavaBean
* @return JsonNode
*/
public static JsonNode parseJsonObject(Object object) {
return objectMapper.valueToTree(object);
}
/**
* JsonNode 转 Json字符串
*
* @param jsonNode JsonNode
* @return Json字符串
*/
public static String toJsonString(JsonNode jsonNode) {
String jsonString = null;
try {
jsonString = objectMapper.writeValueAsString(jsonNode);
} catch (JsonProcessingException e) {
log.error("失败:{}", e.getMessage());
}
return jsonString;
}
}

View File

@ -0,0 +1,110 @@
package cn.octopusyan.alistgui.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
/**
* 命令工具类
*
* @author octopus_yan@foxmail.com
*/
@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 OnExecuteListener listener;
private CommandLine commandLine;
private static final Set<ProcessesUtil> set = new HashSet<>();
/**
* Prevent construction.
*/
private ProcessesUtil(String workingDirectory) {
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(EXIT_VALUE);
processDestroyer = new ShutdownHookProcessDestroyer();
executor.setProcessDestroyer(processDestroyer);
}
public static ProcessesUtil init(String workingDirectory) {
ProcessesUtil util = new ProcessesUtil(workingDirectory);
set.add(util);
return util;
}
public boolean exec(String command) {
commandLine = CommandLine.parse(command);
int execute = 0;
try {
executor.execute(commandLine, new DefaultExecuteResultHandler());
} 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);
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
if (listener != null) {
listener.onExecuteSuccess(exitValue == EXIT_VALUE);
}
}
@Override
public void onProcessFailed(ExecuteException e) {
if (listener != null) {
listener.onExecuteError(e);
}
}
};
try {
executor.execute(commandLine, handler);
} catch (Exception e) {
if (listener != null) listener.onExecuteError(e);
}
}
public void destroy() {
if (processDestroyer.isEmpty()) return;
processDestroyer.run();
}
public boolean isRunning() {
return !processDestroyer.isEmpty();
}
public static void destroyAll() {
set.forEach(ProcessesUtil::destroy);
}
public interface OnExecuteListener {
void onExecute(String msg);
default void onExecuteSuccess(boolean success) {
}
default void onExecuteError(Exception e) {
}
}
}

View File

@ -0,0 +1,77 @@
package cn.octopusyan.alistgui.util;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 配置文件读取工具
*
* @author liubin5620
* @see <a href="https://blog.csdn.net/liubin5620/article/details/104618950">配置文件信息读取工具类【PropertiesUtils】</a>
*/
public class PropertiesUtils {
/**
* 主配置文件
*/
private final Properties properties;
/**
* 启用配置文件
*/
private final Properties propertiesCustom;
private static PropertiesUtils propertiesUtils = new PropertiesUtils();
public static final Logger logger = LoggerFactory.getLogger(PropertiesUtils.class);
/**
* 私有构造,禁止直接创建
*/
private PropertiesUtils() {
// 读取配置启用的配置文件名
properties = new Properties();
propertiesCustom = new Properties();
InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("application.properties");
try {
properties.load(in);
// 加载启用的配置
String property = properties.getProperty("profiles.active");
if (!StringUtils.isBlank(property)) {
InputStream cin = PropertiesUtils.class.getClassLoader().getResourceAsStream("application-" + property + ".properties");
propertiesCustom.load(cin);
}
} catch (IOException e) {
logger.error("读取配置文件失败", e);
}
}
/**
* 获取单例
*
* @return PropertiesUtils
*/
public static PropertiesUtils getInstance() {
if (propertiesUtils == null) {
propertiesUtils = new PropertiesUtils();
}
return propertiesUtils;
}
/**
* 根据属性名读取值
* 先去主配置查询,如果查询不到,就去启用配置查询
*
* @param name 名称
*/
public String getProperty(String name) {
String val = properties.getProperty(name);
if (StringUtils.isBlank(val)) {
val = propertiesCustom.getProperty(name);
}
return val;
}
}

View File

@ -0,0 +1,78 @@
package cn.octopusyan.alistgui.util;
import lombok.Getter;
/**
* 注册表编辑
*
* @author octopus_yan
*/
public class Registry {
private static final ProcessesUtil util = ProcessesUtil.init(".");
public static void setStringValue(Root root, String keyPath, String name, String value) {
setValue(root, keyPath, name, DataType.REG_SZ, value);
}
public static void deleteValue(Root root, String keyPath, String name) {
name = handleSpaces(name);
util.exec(STR."reg \{Operation.DELETE} \{root.path}\\\{keyPath} /v \{name} /f");
}
public static void setValue(Root root, String keyPath, String name, DataType type, String value) {
name = handleSpaces(name);
value = handleSpaces(value);
util.exec(STR."""
reg \{Operation.ADD} \{root.path}\\\{keyPath} /v \{name} /t \{type} /d \{value}
""");
}
private static String handleSpaces(String str) {
if (str.contains(" "))
str = STR."\"\{str}\"";
return str;
}
public enum Operation {
ADD,
COMPARE,
COPY,
DELETE,
EXPORT,
IMPORT,
LOAD,
QUERY,
RESTORE,
SAVE,
UNLOAD,
}
@Getter
public enum Root {
HKCR("HKEY_CLASSES_ROOT"),
HKCU("HKEY_CURRENT_USER"),
HKLM("HKEY_LOCAL_MACHINE"),
HKU("HKEY_USERS"),
HKCC("HKEY_CURRENT_CONFIG"),
;
private final String path;
Root(String path) {
this.path = path;
}
}
public enum DataType {
REG_SZ,
REG_MULTI_SZ,
REG_DWORD_BIG_ENDIAN,
REG_DWORD,
REG_BINARY,
REG_DWORD_LITTLE_ENDIAN,
REG_LINK,
REG_FULL_RESOURCE_DESCRIPTOR,
REG_EXPAND_SZ,
}
}

View File

@ -0,0 +1,69 @@
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 ViewUtil {
// 获取系统缩放比
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("""
-fx-background-radius: 5;
-fx-border-radius: 5;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 15, 0, 0, 0);
-fx-background-insets: 20;
-fx-padding: 20;
""");
}
public static void bindDragged(Pane pane) {
Stage stage = getStage(pane);
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());
paneYOffset.put(pane, stage.getY() - event.getScreenY());
});
pane.setOnMouseDragged(event -> {
stage.setX(event.getScreenX() + paneXOffset.get(pane));
stage.setY(event.getScreenY() + paneYOffset.get(pane));
});
}
public static Stage getStage() {
return Application.getPrimaryStage();
}
public static Stage getStage(Pane pane) {
try {
return (Stage) pane.getScene().getWindow();
} catch (Throwable e) {
return getStage();
}
}
}

View File

@ -0,0 +1,137 @@
package cn.octopusyan.alistgui.view;
import atlantafx.base.controls.CaptionMenuItem;
import cn.octopusyan.alistgui.config.Constants;
import cn.octopusyan.alistgui.util.ViewUtil;
import javafx.application.Platform;
import javafx.beans.binding.StringBinding;
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(StringBinding bind, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem();
menuItem.textProperty().bind(bind);
return addItem(menuItem, 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(StringBinding label, MenuItem... items) {
Menu menu = new Menu();
menu.textProperty().bind(label);
return addMenu(menu, 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() / ViewUtil.scaleX,
event.getY() / ViewUtil.scaleY
);
// 获取焦点 (失去焦点隐藏自身)
root.requestFocus();
}
public static MenuItem menuItem(String label, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem(label);
menuItem.setOnAction(handler);
return menuItem;
}
public static MenuItem menuItem(StringBinding stringBinding, EventHandler<ActionEvent> handler) {
MenuItem menuItem = new MenuItem();
menuItem.textProperty().bind(stringBinding);
menuItem.setOnAction(handler);
return menuItem;
}
}

View File

@ -0,0 +1,23 @@
package cn.octopusyan.alistgui.view;
import cn.octopusyan.alistgui.enums.ProxySetup;
import javafx.scene.control.ListCell;
/**
* ProxySetup I18n Cell
*
* @author octopus_yan
*/
public class ProxySetupCell extends ListCell<ProxySetup> {
@Override
protected void updateItem(ProxySetup item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty || item == null) {
setText("");
} else {
textProperty().bind(item.getBinding());
}
}
}

View File

@ -0,0 +1,96 @@
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;
import javafx.stage.Window;
/**
* 弹窗工具
*
* @author octopus_yan@foxmail.com
*/
public class AlertUtil {
private static Window mOwner;
public static void initOwner(Stage stage) {
AlertUtil.mOwner = stage;
}
public static DefaultBuilder builder() {
return new DefaultBuilder(mOwner);
}
public static AlertBuilder info(String content) {
return info().content(content).header(null);
}
public static AlertBuilder info() {
return alert(Alert.AlertType.INFORMATION);
}
public static AlertBuilder error(String message) {
return alert(Alert.AlertType.ERROR).header(null).content(message);
}
public static AlertBuilder warning() {
return alert(Alert.AlertType.WARNING);
}
public static AlertBuilder exception(Exception ex) {
return alert(Alert.AlertType.ERROR).exception(ex);
}
/**
* 确认对话框
*/
public static AlertBuilder confirm() {
return alert(Alert.AlertType.CONFIRMATION);
}
/**
* 自定义确认对话框 <p>
*
* @param buttons <code>"Cancel"</code> OR <code>"取消"</code> 为取消按钮
*/
public static AlertBuilder confirm(String... buttons) {
return confirm().buttons(buttons);
}
public static AlertBuilder confirm(ButtonType... buttons) {
return confirm().buttons(buttons);
}
public static AlertBuilder alert(Alert.AlertType type) {
return new AlertBuilder(mOwner, type);
}
public static TextInputBuilder input(String content) {
return new TextInputBuilder(mOwner);
}
public static TextInputBuilder input(String content, String defaultResult) {
return new TextInputBuilder(mOwner, defaultResult).content(content);
}
@SafeVarargs
public static <T> ChoiceBuilder<T> choices(String hintText, T... choices) {
return new ChoiceBuilder<>(mOwner, choices).content(hintText);
}
public static ProgressBuilder progress() {
return new ProgressBuilder(mOwner);
}
public interface OnChoseListener {
void confirm();
default void cancelOrClose(ButtonType buttonType) {
}
}
public interface OnClickListener {
void onClicked(String result);
}
}

View File

@ -0,0 +1,109 @@
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;
import javafx.stage.Window;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author octopus_yan
*/
public class AlertBuilder extends BaseBuilder<AlertBuilder, Alert> {
public AlertBuilder(Window owner, Alert.AlertType alertType) {
super(new Alert(alertType), owner);
}
public AlertBuilder buttons(String... buttons) {
dialog.getButtonTypes().addAll(getButtonList(buttons));
return this;
}
public AlertBuilder buttons(ButtonType... buttons) {
dialog.getButtonTypes().addAll(buttons);
return this;
}
public AlertBuilder exception(Exception ex) {
dialog.setTitle("Exception Dialog");
dialog.setHeaderText(ex.getClass().getSimpleName());
dialog.setContentText(ex.getMessage());
// 创建可扩展的异常。
var sw = new StringWriter();
var pw = new PrintWriter(sw);
ex.printStackTrace(pw);
var exceptionText = sw.toString();
var label = new Label("The exception stacktrace was :");
var textArea = new TextArea(exceptionText);
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMaxHeight(Double.MAX_VALUE);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
var expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
// 将可扩展异常设置到对话框窗格中。
dialog.getDialogPane().setExpandableContent(expContent);
return this;
}
/**
* 获取按钮列表
*
* @param buttons "Cancel" / "取消" 为取消按钮
*/
private List<ButtonType> getButtonList(String[] buttons) {
if (ArrayUtils.isEmpty(buttons)) return Collections.emptyList();
return Arrays.stream(buttons).map((type) -> {
ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER;
if ("cancel".equals(StringUtils.lowerCase(type)) || "取消".equals(type)) {
return ButtonType.CANCEL;
}
return new ButtonType(type, buttonData);
}).collect(Collectors.toList());
}
/**
* AlertUtil.confirm
*/
public void show(AlertUtil.OnClickListener listener) {
Optional<ButtonType> result = dialog.showAndWait();
result.ifPresent(r -> listener.onClicked(r.getText()));
}
/**
* AlertUtil.confirm
*/
public void show(AlertUtil.OnChoseListener listener) {
Optional<ButtonType> result = dialog.showAndWait();
result.ifPresent(r -> {
if (r == ButtonType.OK) {
listener.confirm();
} else {
listener.cancelOrClose(r);
}
});
}
}

View File

@ -0,0 +1,30 @@
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.Optional;
/**
* @author octopus_yan
*/
public class ChoiceBuilder<R> extends BaseBuilder<ChoiceBuilder<R>, ChoiceDialog<R>> {
@SafeVarargs
public ChoiceBuilder(Window mOwner, R... choices) {
this(new ChoiceDialog<>(choices[0], choices), mOwner);
}
public ChoiceBuilder(ChoiceDialog<R> dialog, Window mOwner) {
super(dialog, mOwner);
}
/**
* AlertUtil.choices
*/
public R showAndGetChoice() {
Optional<R> result = dialog.showAndWait();
return result.orElse(null);
}
}

View File

@ -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.ViewUtil;
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<DefaultBuilder, Dialog<?>> {
public DefaultBuilder(Window mOwner) {
super(new Dialog<>(), mOwner);
header(null);
DialogPane dialogPane = dialog.getDialogPane();
dialogPane.getScene().setFill(Color.TRANSPARENT);
ViewUtil.bindDragged(dialogPane);
ViewUtil.bindShadow(dialogPane);
ViewUtil.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;
}
}

View File

@ -0,0 +1,52 @@
package cn.octopusyan.alistgui.view.alert.builder;
import cn.octopusyan.alistgui.config.Context;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Window;
/**
* 加载弹窗
*
* @author octopus_yan
*/
public class ProgressBuilder extends DefaultBuilder {
public ProgressBuilder(Window mOwner) {
super(mOwner);
content(getContent());
}
private Pane getContent() {
HBox hBox = new HBox();
hBox.setPrefWidth(350);
hBox.setAlignment(Pos.CENTER);
hBox.setPadding(new Insets(10, 0, 10, 0));
// 取消按钮
Button cancel = new Button(Context.getLanguageBinding("label.cancel").get());
cancel.setCancelButton(true);
cancel.setOnAction(_ -> dialog.close());
// 进度条 TODO 宽度绑定
ProgressBar progressBar = new ProgressBar(-1);
progressBar.prefWidthProperty().bind(Bindings.createDoubleBinding(
() -> hBox.widthProperty().get() - cancel.widthProperty().get() - 40,
hBox.widthProperty(), cancel.widthProperty()
));
hBox.getChildren().add(progressBar);
hBox.getChildren().add(cancel);
return hBox;
}
public ProgressBuilder onCancel(Runnable run) {
dialog.setOnCloseRequest(_ -> run.run());
return this;
}
}

View File

@ -0,0 +1,36 @@
package cn.octopusyan.alistgui.view.alert.builder;
import cn.octopusyan.alistgui.base.BaseBuilder;
import javafx.scene.control.TextInputDialog;
import javafx.stage.Window;
import java.util.Optional;
/**
* 获取用户输入弹窗
*
* @author octopus_yan
*/
public class TextInputBuilder extends BaseBuilder<TextInputBuilder, TextInputDialog> {
public TextInputBuilder(Window mOwner) {
this(new TextInputDialog(), mOwner);
}
public TextInputBuilder(Window mOwner, String defaultResult) {
this(new TextInputDialog(defaultResult), mOwner);
}
public TextInputBuilder(TextInputDialog dialog, Window mOwner) {
super(dialog, mOwner);
}
/**
* AlertUtil.input
* 如果用户点击了取消按钮,将会返回null
*/
public String getInput() {
Optional<String> result = dialog.showAndWait();
return result.orElse(null);
}
}

View File

@ -0,0 +1,166 @@
package cn.octopusyan.alistgui.viewModel;
import cn.octopusyan.alistgui.base.BaseViewModel;
import cn.octopusyan.alistgui.config.Constants;
import cn.octopusyan.alistgui.config.Context;
import cn.octopusyan.alistgui.manager.AListManager;
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.Gui;
import cn.octopusyan.alistgui.model.upgrade.UpgradeApp;
import cn.octopusyan.alistgui.task.CheckUpdateTask;
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 cn.octopusyan.alistgui.view.alert.builder.AlertBuilder;
import javafx.application.Platform;
import javafx.beans.property.*;
import lombok.extern.slf4j.Slf4j;
/**
* 关于
*
* @author octopus_yan
*/
@Slf4j
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);
private final StringProperty guiVersion = new SimpleStringProperty(ConfigManager.guiVersion());
private final StringProperty guiNewVersion = new SimpleStringProperty("");
private final BooleanProperty guiUpgrade = new SimpleBooleanProperty(false);
public AboutViewModule() {
aListVersion.bindBidirectional(ConfigManager.aListVersionProperty());
}
public Property<String> aListVersionProperty() {
return aListVersion;
}
public BooleanProperty aListUpgradeProperty() {
return aListUpgrade;
}
public StringProperty aListNewVersionProperty() {
return aListNewVersion;
}
public StringProperty guiVersionProperty() {
return guiVersion;
}
public StringProperty guiNewVersionProperty() {
return guiNewVersion;
}
public BooleanProperty guiUpgradeProperty() {
return guiUpgrade;
}
/**
* 检查更新
*/
public void checkUpdate(UpgradeApp app) {
// 检查任务
startUpgrade(app, () -> onChecked(app));
}
/**
* 开始检查更新
*
* @param app 更新的应用
* @param runnable 检查后执行的任务
*/
private void startUpgrade(UpgradeApp app, Runnable runnable) {
// 检查更新的任务
var task = new CheckUpdateTask(app);
// 任务监听
task.onListen(new TaskListener.UpgradeListener(task) {
@Override
protected void onSucceed() {
if (runnable != null) runnable.run();
}
@Override
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();
}
private void onChecked(UpgradeApp app) {
// 判断 检查的应用
boolean tag = app instanceof AList;
boolean upgrade = tag ? aListUpgrade.get() : guiUpgrade.get();
String version = tag ? aListVersion.get() : guiVersion.get();
String newVersion = tag ? aListNewVersion.get() : guiNewVersion.get();
String title = Context.getLanguageBinding(STR."about.\{tag ? "alist" : "app"}.update").getValue();
String currentLabel = Context.getLanguageBinding("update.current").get();
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)
.content(STR."""
\{currentLabel} : \{version}
\{newLabel} : \{newVersion}
""")
.show(() -> {
// 可升级,且点击了确定后,开始下载任务
if (upgrade)
DownloadUtil.startDownload(app, newVersion, () -> {
Platform.runLater(() -> {
switch (app) {
case AList _ -> {
// 下载完成后,解压并删除文件
DownloadUtil.unzip(app);
// 设置应用版本
aListVersion.setValue(aListNewVersion.getValue());
AListManager.restart();
}
case Gui _ -> {
log.info(STR."guiNewVersion => \{guiNewVersion.get()}");
// 启动升级程序
ProcessesUtil.init(Constants.DATA_DIR_PATH).exec("upgrade.exe");
Platform.exit();
}
default -> throw new IllegalStateException(STR."Unexpected value: \{app}");
}
});
}).execute();
});
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,24 @@
package cn.octopusyan.alistgui.viewModel;
import cn.octopusyan.alistgui.base.BaseViewModel;
import cn.octopusyan.alistgui.manager.AListManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
/**
* 主界面VM
*
* @author octopus_yan
*/
public class MainViewModel extends BaseViewModel {
private final BooleanProperty running = new SimpleBooleanProperty();
public MainViewModel() {
// 先添加监听再绑定解决切换locale后界面状态显示错误的问题
running.bind(AListManager.runningProperty());
}
public BooleanProperty runningProperty() {
return running;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,144 @@
package cn.octopusyan.alistgui.viewModel;
import atlantafx.base.theme.Theme;
import cn.octopusyan.alistgui.base.BaseViewModel;
import cn.octopusyan.alistgui.config.Constants;
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.task.listener.TaskListener;
import cn.octopusyan.alistgui.util.Registry;
import cn.octopusyan.alistgui.view.alert.AlertUtil;
import javafx.beans.property.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Locale;
/**
* 设置视图数据
*
* @author octopus_yan
*/
@Slf4j
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());
private final ObjectProperty<Locale> language = new SimpleObjectProperty<>(ConfigManager.language());
private final ObjectProperty<ProxySetup> proxySetup = new SimpleObjectProperty<>(ConfigManager.proxySetup());
private final StringProperty proxyTestUrl = new SimpleStringProperty(ConfigManager.proxyTestUrl());
public SetupViewModel() {
theme.addListener((_, _, newValue) -> ConfigManager.theme(newValue));
silentStartup.addListener((_, _, newValue) -> ConfigManager.silentStartup(newValue));
autoStart.addListener((_, _, newValue) -> {
try {
if (newValue) {
Registry.setStringValue(Registry.Root.HKCU, Constants.REG_AUTO_RUN, Constants.APP_TITLE, Constants.APP_EXE);
} else {
Registry.deleteValue(Registry.Root.HKCU, Constants.REG_AUTO_RUN, Constants.APP_TITLE);
}
} catch (Throwable e) {
log.error("", e);
}
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));
proxyPort.addListener((_, _, newValue) -> ConfigManager.proxyPort(newValue));
language.addListener((_, _, newValue) -> Context.setLanguage(newValue));
}
public ObjectProperty<Theme> themeProperty() {
return theme;
}
public BooleanProperty autoStartProperty() {
return autoStart;
}
public BooleanProperty silentStartupProperty() {
return silentStartup;
}
public BooleanProperty closeToTrayProperty() {
return closeToTray;
}
public ObjectProperty<Locale> languageProperty() {
return language;
}
public ObjectProperty<ProxySetup> proxySetupProperty() {
return proxySetup;
}
public StringProperty proxyHostProperty() {
return proxyHost;
}
public StringProperty proxyPortProperty() {
return proxyPort;
}
public void proxyTest() {
var checkUrl = AlertUtil.input("URL :", proxyTestUrl.getValue())
.title(Context.getLanguageBinding("proxy.test.title").getValue())
.header(Context.getLanguageBinding("proxy.test.header").getValue())
.getInput();
if (StringUtils.isEmpty(checkUrl)) return;
proxyTestUrl.setValue(checkUrl);
ConfigManager.checkProxy((success, msg) -> {
if (!success) {
final var tmp = Context.getLanguageBinding("proxy.test.result.failed").getValue();
AlertUtil.error(STR."\{tmp}\{msg}").show();
return;
}
HttpUtil.getInstance().proxy(ConfigManager.proxySetup(), ConfigManager.getProxyInfo());
getProxyCheckTask(checkUrl).execute();
});
}
private static ProxyCheckTask getProxyCheckTask(String checkUrl) {
var task = new ProxyCheckTask(checkUrl);
task.onListen(new TaskListener(task) {
@Override
public void onSucceed() {
AlertUtil.info(Context.getLanguageBinding("proxy.test.result.success").getValue()).show();
}
@Override
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();
}
});
return task;
}
}

View File

@ -0,0 +1,28 @@
module cn.octopusyan.alistgui {
requires java.desktop;
requires java.net.http;
requires javafx.controls;
requires javafx.fxml;
requires javafx.graphics;
requires org.apache.commons.lang3;
requires org.apache.commons.exec;
requires org.slf4j;
requires ch.qos.logback.core;
requires ch.qos.logback.classic;
requires cn.hutool.core;
requires org.kordamp.ikonli.javafx;
requires org.kordamp.ikonli.fontawesome;
requires com.gluonhq.emoji;
requires static lombok;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.dataformat.yaml;
requires atlantafx.base;
exports cn.octopusyan.alistgui;
opens cn.octopusyan.alistgui to javafx.fxml;
opens cn.octopusyan.alistgui.model to com.fasterxml.jackson.databind;
opens cn.octopusyan.alistgui.controller to javafx.fxml;
opens cn.octopusyan.alistgui.base to com.fasterxml.jackson.databind;
opens cn.octopusyan.alistgui.model.upgrade to com.fasterxml.jackson.databind;
exports cn.octopusyan.alistgui.model.upgrade;
}

View File

@ -0,0 +1,3 @@
app.name=${project.name}
app.title=AList GUI
app.version=v${project.version}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,14 @@
<svg width="1252" height="1252" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<g id="#70c6beff">
<path id="svg_2"
d="m634.37,138.38c11.88,-1.36 24.25,1.3 34.18,8.09c14.96,9.66 25.55,24.41 34.49,39.51c40.59,68.03 81.45,135.91 122.02,203.96c54.02,90.99 108.06,181.97 161.94,273.06c37.28,63 74.65,125.96 112.18,188.82c24.72,41.99 50.21,83.54 73.84,126.16c10.18,17.84 15.77,38.44 14.93,59.03c-0.59,15.92 -3.48,32.28 -11.84,46.08c-11.73,19.46 -31.39,33.2 -52.71,40.36c-11.37,4.09 -23.3,6.87 -35.43,6.89c-132.32,-0.05 -264.64,0.04 -396.95,0.03c-11.38,-0.29 -22.95,-1.6 -33.63,-5.72c-7.81,-3.33 -15.5,-7.43 -21.61,-13.42c-10.43,-10.32 -17.19,-24.96 -15.38,-39.83c0.94,-10.39 3.48,-20.64 7.76,-30.16c4.15,-9.77 9.99,-18.67 15.06,-27.97c22.13,-39.47 45.31,-78.35 69.42,-116.65c7.72,-12.05 14.44,-25.07 25.12,-34.87c11.35,-10.39 25.6,-18.54 41.21,-19.6c12.55,-0.52 24.89,3.82 35.35,10.55c11.8,6.92 21.09,18.44 24.2,31.88c4.49,17.01 -0.34,34.88 -7.55,50.42c-8.09,17.65 -19.62,33.67 -25.81,52.18c-1.13,4.21 -2.66,9.52 0.48,13.23c3.19,3 7.62,4.18 11.77,5.22c12,2.67 24.38,1.98 36.59,2.06c45,-0.01 90,0 135,0c8.91,-0.15 17.83,0.3 26.74,-0.22c6.43,-0.74 13.44,-1.79 18.44,-6.28c3.3,-2.92 3.71,-7.85 2.46,-11.85c-2.74,-8.86 -7.46,-16.93 -12.12,-24.89c-119.99,-204.91 -239.31,-410.22 -360.56,-614.4c-3.96,-6.56 -7.36,-13.68 -13.03,-18.98c-2.8,-2.69 -6.95,-4.22 -10.77,-3.11c-3.25,1.17 -5.45,4.03 -7.61,6.57c-5.34,6.81 -10.12,14.06 -14.51,21.52c-20.89,33.95 -40.88,68.44 -61.35,102.64c-117.9,198.43 -235.82,396.85 -353.71,595.29c-7.31,13.46 -15.09,26.67 -23.57,39.43c-7.45,10.96 -16.49,21.23 -28.14,27.83c-13.73,7.94 -30.69,11.09 -46.08,6.54c-11.23,-3.47 -22.09,-9.12 -30.13,-17.84c-10.18,-10.08 -14.69,-24.83 -14.17,-38.94c0.52,-14.86 5.49,-29.34 12.98,-42.1c71.58,-121.59 143.62,-242.92 215.93,-364.09c37.2,-62.8 74.23,-125.69 111.64,-188.36c37.84,-63.5 75.77,-126.94 113.44,-190.54c21.02,-35.82 42.19,-71.56 64.28,-106.74c6.79,-11.15 15.58,-21.15 26.16,-28.85c8.68,-5.92 18.42,-11 29.05,-11.94z"
fill="#70c6be"/>
</g>
<g id="#1ba0d8ff">
<path id="svg_3"
d="m628.35,608.38c17.83,-2.87 36.72,1.39 51.5,11.78c11.22,8.66 19.01,21.64 21.26,35.65c1.53,10.68 0.49,21.75 -3.44,31.84c-3.02,8.73 -7.35,16.94 -12.17,24.81c-68.76,115.58 -137.5,231.17 -206.27,346.75c-8.8,14.47 -16.82,29.47 -26.96,43.07c-7.37,9.11 -16.58,16.85 -27.21,21.89c-22.47,11.97 -51.79,4.67 -68.88,-13.33c-8.66,-8.69 -13.74,-20.63 -14.4,-32.84c-0.98,-12.64 1.81,-25.42 7.53,-36.69c5.03,-10.96 10.98,-21.45 17.19,-31.77c30.22,-50.84 60.17,-101.84 90.3,-152.73c41.24,-69.98 83.16,-139.55 124.66,-209.37c4.41,-7.94 9.91,-15.26 16.09,-21.9c8.33,-8.46 18.9,-15.3 30.8,-17.16z"
fill="#1ba0d8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,22 @@
/**************************************************
* About View
**************************************************/
.shield {
.label {
-fx-text-fill: white;
-fx-label-padding: 3 5 3 5;
}
.shield-name {
-fx-background-color: #555555;
-fx-background-radius: 5 0 0 5;
}
.shield-version {
-fx-background-color: #6969AA;
-fx-background-radius: 0 5 5 0;
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,71 @@
/**************************************************
* Main View
**************************************************/
#homeLabel {
-fx-font-size: 35;
-fx-font-weight: bold;
}
#statusLabel {
-fx-padding: 2 5 2 5;
-fx-font-size: 15;
-fx-background-radius: 10;
-fx-text-alignment: CENTER;
-fx-border-radius: 10;
}
.control-menu, #moreButton {
-fx-font-size: 15;
-fx-background-radius: 15;
-fx-padding: 10 40;
-fx-border-radius: 15;
-fx-border-width: 2;
}
#startButton {
-color-button-bg-focused: -color-button-bg;
-fx-border-color: -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-pressed: -color-button-bg;
-fx-border-color: -color-button-bg;
}
#restartButton {
-color-button-bg: linear-gradient(to bottom right, -color-accent-3, -color-chart-6);
-color-button-bg-hover: -color-button-bg;
-color-button-bg-focused: -color-button-bg;
-color-button-bg-pressed: -color-button-bg;
-fx-border-color: -color-button-bg;
}
#moreButton {
-fx-padding: 3 30;
-fx-background-color: transparent;
-fx-border-color: -color-chart-6-alpha70;
-color-button-fg: -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 {
-fx-font-family: "Lucida Console";
-fx-font-size: 15;
-fx-background-radius: 15;
-fx-border-radius: 15;
-fx-padding: 5 15 5 15;
-fx-background-color: -color-neutral-muted;
}

View File

@ -0,0 +1,112 @@
@import "root.css";
/**************************************************
* Window Header
**************************************************/
#windowHeader {
.icon-button {
-fx-icon-code: fa-circle;
-fx-opacity: 0.5;
&:hover {
-fx-opacity: 1.0;
}
}
#closeIcon {
-fx-icon-color: -color-chart-1;
}
#minimizeIcon {
-fx-icon-color: -color-chart-2;
}
#alwaysOnTopIcon {
-fx-icon-color: -color-chart-3;
&:always-on-top {
-fx-opacity: 1.0;
}
}
}
/**************************************************
* Tab label
**************************************************/
#tabPane {
-fx-background-color: transparent;
.tab-header-area {
-fx-background-color: transparent;
.tab-header-background {
-fx-background-color: transparent;
}
.headers-region {
//-fx-background-color: #f9f9fb;
-fx-background-color: -color-neutral-muted;
-fx-background-radius: 10;
.tab {
-fx-padding: 5;
-fx-background-color: transparent;
.tab-container {
-fx-background-color: transparent;
-fx-border-width: 0;
.tab-label {
-fx-pref-width: 80;
-fx-padding: 10 0;
-fx-background-radius: 10;
-fx-text-alignment: CENTER;
-fx-alignment: CENTER;
-fx-font-size: 15px;
-fx-border-width: 0;
}
}
&:selected {
.tab-label {
-fx-background-color: -color-accent-5;
-fx-text-fill: white;
}
.ikonli-font-icon {
-fx-icon-color: white;
}
}
}
}
}
}
/**************************************************
* Window Footer
**************************************************/
#windowFooter {
.button {
-fx-font-size: 15;
-fx-text-alignment: CENTER;
}
.ikonli-font-icon {
-fx-font-size: 15;
}
}
/**************************************************
* Modal Pane
**************************************************/
.modal-pane {
-fx-background-radius: 15;
.scrollable-content {
-fx-background-radius: 15;
}
}

View File

@ -0,0 +1,20 @@
/**************************************************
* Root
**************************************************/
.root {
-fx-font-size: 15;
-fx-font-weight: bolder;
}
.root-pane {
-fx-background-radius: 15;
-fx-border-radius: 15;
// 窗口阴影
//-fx-background-color: rgba(255, 255, 255, 1);
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 15, 0, 0, 0);
-fx-background-insets: 20;
-fx-padding: 20;
}

View File

@ -0,0 +1,27 @@
/**************************************************
* Setup View
**************************************************/
#setupView {
.check-box {
-fx-font-size: 15;
}
.proxy-panel {
-fx-background-color: -color-neutral-muted;
-fx-background-radius: 15;
-fx-border-radius: 15;
-fx-border-width: 5;
.radio-button {
-fx-background-color: transparent;
}
}
.proxy-label {
-fx-font-size: 15;
-fx-text-fill: -color-accent-5;
-fx-text-alignment: CENTER;
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<VBox fx:id="aboutView" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
prefHeight="700" prefWidth="720" spacing="30" alignment="CENTER"
stylesheets="@../css/about-view.css"
fx:controller="cn.octopusyan.alistgui.controller.AboutController">
<StackPane>
<padding>
<Insets bottom="10"/>
</padding>
<ImageView pickOnBounds="true" preserveRatio="true">
<Image url="@../assets/logo-about.png" backgroundLoading="true"/>
</ImageView>
</StackPane>
<HBox alignment="CENTER" styleClass="shield">
<Label fx:id="aListVersionLabel" styleClass="shield-name" text="%about.alist.version"/>
<Label fx:id="aListVersion" styleClass="shield-version"/>
</HBox>
<HBox alignment="CENTER" styleClass="shield">
<Label fx:id="appVersionLabel" styleClass="shield-name" text="%about.app.version"/>
<Label styleClass="shield-version" text="v${project.version}"/>
</HBox>
<Button fx:id="checkAppVersion" onAction="#checkGuiUpdate" styleClass="flat" text="%about.app.update"/>
<Button fx:id="checkAListVersion" onAction="#checkAListUpdate" styleClass="flat" text="%about.alist.update"/>
</VBox>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import atlantafx.base.layout.InputGroup?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.*?>
<AnchorPane id="admin-panel" fx:id="adminPanel" maxHeight="250" maxWidth="520" prefHeight="250.0" prefWidth="520.0"
stylesheets="@../css/admin-panel.css" xmlns="http://javafx.com/javafx/11.0.14-internal"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="cn.octopusyan.alistgui.controller.PasswordController">
<AnchorPane styleClass="header" prefWidth="520" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0"
AnchorPane.topAnchor="0">
<Label text="%admin.pwd.title" AnchorPane.leftAnchor="10" AnchorPane.topAnchor="10"/>
<Button onAction="#close" styleClass="flat" AnchorPane.rightAnchor="0" AnchorPane.topAnchor="0">
<graphic>
<FontIcon iconLiteral="fa-remove"/>
</graphic>
</Button>
</AnchorPane>
<VBox alignment="CENTER" spacing="20"
AnchorPane.bottomAnchor="30" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0">
<Label fx:id="toptip" style="-fx-background-radius: 10;-fx-background-color: -color-button-bg-hover;"
styleClass="admin-toptip, button, flat, danger" text="%admin.pwd.toptip"/>
<Pane style="-fx-background-color: transparent"/>
<HBox alignment="CENTER" styleClass="admin-field">
<Label fx:id="usernameLabel" text="%admin.pwd.user-field"/>
<InputGroup fx:id="userField" styleClass="admin-field-value">
<TextField fx:id="usernameField" text="admin" editable="false"/>
<Button fx:id="copyUsername" onAction="#copyUsername">
<graphic>
<FontIcon iconLiteral="fa-copy"/>
</graphic>
</Button>
</InputGroup>
</HBox>
<HBox alignment="CENTER" styleClass="admin-field">
<Label fx:id="passwordLabel" text="%admin.pwd.pwd-field"/>
<InputGroup styleClass="admin-field-value">
<PasswordField fx:id="passwordField" editable="false"/>
<Button fx:id="refreshPassword" onAction="#savePassword" visible="false" managed="false">
<graphic>
<FontIcon iconLiteral="fa-refresh"/>
</graphic>
</Button>
<Button fx:id="savePassword" onAction="#savePassword" visible="false" managed="false">
<graphic>
<FontIcon iconLiteral="fa-save"/>
</graphic>
</Button>
<Button fx:id="copyPassword" onAction="#copyPassword">
<graphic>
<FontIcon iconLiteral="fa-copy"/>
</graphic>
</Button>
</InputGroup>
</HBox>
</VBox>
</AnchorPane>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox fx:id="mainView" prefHeight="700" prefWidth="720" stylesheets="@../css/main-view.css"
xmlns="http://javafx.com/javafx/11.0.14-internal" xmlns:fx="http://javafx.com/fxml/1"
alignment="TOP_CENTER"
fx:controller="cn.octopusyan.alistgui.controller.MainController">
<padding>
<Insets left="10.0" right="10.0" top="10.0"/>
</padding>
<HBox alignment="TOP_CENTER" prefWidth="Infinity">
<Label fx:id="homeLabel" alignment="CENTER" text="AList GUI"/>
<Button fx:id="statusLabel" styleClass="danger" alignment="TOP_CENTER" text="%main.status.label-stop">
<HBox.margin>
<Insets left="-10.0" top="-5"/>
</HBox.margin>
</Button>
</HBox>
<HBox alignment="TOP_CENTER" prefWidth="Infinity" spacing="25.0">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<VBox.margin>
<Insets bottom="10.0" top="10.0"/>
</VBox.margin>
<Button fx:id="startButton" onAction="#start" styleClass="control-menu, success"
text="%main.control.start"/>
<Button fx:id="passwordButton" onAction="#adminPassword" styleClass="control-menu, success"
text="%main.control.password"/>
<Button fx:id="restartButton" onAction="#restart" styleClass="control-menu, success"
text="%main.control.restart"/>
<MenuButton fx:id="moreButton" styleClass="button-outlined, no-arrow" text="%main.control.more">
<items>
<MenuItem fx:id="browserButton" onAction="#openInBrowser" disable="true" text="%main.more.browser"/>
<MenuItem fx:id="configButton" onAction="#openConfig" text="%main.more.open-config"/>
<MenuItem fx:id="logButton" onAction="#openLogFolder" text="%main.more.open-log"/>
</items>
</MenuButton>
</HBox>
<ScrollPane fx:id="logAreaSp" fitToWidth="true" prefHeight="499.0" prefWidth="Infinity"
styleClass="logArea" VBox.vgrow="ALWAYS">
<VBox fx:id="logArea" spacing="10">
<VBox.margin>
<Insets bottom="10.0" top="10.0"/>
</VBox.margin>
</VBox>
</ScrollPane>
</VBox>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.*?>
<StackPane fx:id="rootPane" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
prefHeight="720.0" prefWidth="770.0"
styleClass="root-pane" stylesheets="@../css/root-view.css"
fx:controller="cn.octopusyan.alistgui.controller.RootController">
<VBox prefHeight="720.0" prefWidth="770.0" spacing="10.0">
<HBox fx:id="windowHeader" alignment="CENTER_RIGHT" prefWidth="Infinity" spacing="10.0">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<FontIcon fx:id="alwaysOnTopIcon" styleClass="icon-button"/>
<FontIcon fx:id="minimizeIcon" styleClass="icon-button"/>
<FontIcon fx:id="closeIcon" styleClass="icon-button"/>
</HBox>
<TabPane fx:id="tabPane" prefWidth="Infinity" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<padding>
<Insets left="20.0" right="20.0"/>
</padding>
<Tab fx:id="mainTab" text="%root.tab.main">
<graphic>
<FontIcon iconColor="white" iconLiteral="fa-th-large"/>
</graphic>
<!-- 引入主页 -->
<fx:include fx:id="mainController" source="main-view.fxml" prefWidth="Infinity" prefHeight="-Infinity"/>
</Tab>
<Tab fx:id="setupTab" text="%root.tab.setup">
<graphic>
<FontIcon iconColor="white" iconLiteral="fa-cog"/>
</graphic>
<!-- 引入设置页 -->
<fx:include fx:id="setupController" source="setup-view.fxml" prefWidth="Infinity"
prefHeight="-Infinity"/>
</Tab>
<Tab fx:id="aboutTab" text="%root.tab.about">
<graphic>
<FontIcon iconColor="white" iconLiteral="fa-info-circle"/>
</graphic>
<!-- 引入关于页 -->
<fx:include fx:id="aboutController" source="about-view.fxml" prefWidth="Infinity"
prefHeight="-Infinity"/>
</Tab>
</TabPane>
<HBox fx:id="windowFooter" alignment="CENTER" prefWidth="Infinity" spacing="25.0">
<padding>
<Insets bottom="30.0"/>
</padding>
<Button fx:id="document" onAction="#openDocument" styleClass="success, flat" text="%root.foot.doc"/>
<Button fx:id="github" onAction="#openGithub" styleClass="accent, flat" text="%root.foot.github"/>
<Button fx:id="sponsor" styleClass="danger, flat" text="%root.foot.sponsor"
visible="false" managed="false"/>
</HBox>
</VBox>
</StackPane>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox fx:id="setupView" stylesheets="@../css/setup-view.css"
prefHeight="700" prefWidth="720" spacing="20"
xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cn.octopusyan.alistgui.controller.SetupController">
<padding>
<Insets left="10.0" right="10.0" top="20.0"/>
</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 fx:id="themeLabel" text="%setup.theme"/>
<ComboBox fx:id="themeComboBox"/>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="10">
<Label fx:id="languageLabel" text="%setup.language"/>
<ComboBox fx:id="languageComboBox"/>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="20">
<Label fx:id="proxySetupLabel" styleClass="proxy-label" text="%setup.proxy"/>
<ComboBox fx:id="proxySetupComboBox"/>
<Button fx:id="proxyCheck" onAction="#proxyTest" text="%setup.proxy.test"/>
</HBox>
<GridPane fx:id="proxySetupPane" vgap="10" hgap="10">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" percentWidth="10"/>
<ColumnConstraints hgrow="SOMETIMES" percentWidth="40"/>
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
</rowConstraints>
<padding>
<Insets left="30"/>
</padding>
<Label fx:id="hostLabel" text="%setup.proxy.host"/>
<TextField fx:id="proxyHost" promptText="127.0.0.1" GridPane.columnIndex="1"/>
<Label fx:id="portLabel" text="%setup.proxy.port" GridPane.rowIndex="1"/>
<TextField fx:id="proxyPort" promptText="8080" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
</GridPane>
</VBox>

View File

@ -0,0 +1,54 @@
label.cancel=\u53D6\u6D88
root.tab.main=\u4E3B\u9875
root.tab.setup=\u8BBE\u7F6E
root.tab.about=\u5173\u4E8E
root.foot.doc=\u6587\u6863
root.foot.github=GitHub
root.foot.sponsor=\u8D5E\u52A9 AList
main.control.start=\u5F00\u59CB
main.control.stop=\u505C\u6B62
main.control.password=\u5BC6\u7801
main.control.restart=\u91CD\u542F
main.control.more=\u66F4\u591A
main.status.label-running=\u8FD0\u884C\u4E2D
main.status.label-stop=\u5DF2\u505C\u6B62
main.more.browser=\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00
main.more.open-config=\u6253\u5F00\u914D\u7F6E\u6587\u4EF6
main.more.open-log=\u6253\u5F00\u65E5\u5FD7\u6587\u4EF6\u5939
alist.status.start.running=AList \u6B63\u5728\u8FD0\u884C\u3002\u3002\u3002
alist.status.start=\u6B63\u5728\u542F\u52A8 AList
alist.status.stop=\u6B63\u5728\u505C\u6B62AList
alist.status.stop.stopped=AList \u5DF2\u505C\u6B62
setup.proxy=HTTP\u4EE3\u7406
setup.auto-start.label=\u5F00\u673A\u81EA\u542F
setup.silent-startup.label=\u9759\u9ED8\u542F\u52A8
setup.language=\u8BED\u8A00
proxy.setup.label.no_proxy=\u4E0D\u4EE3\u7406
proxy.setup.label.system=\u7CFB\u7EDF\u4EE3\u7406
proxy.setup.label.manual=\u624B\u52A8\u8BBE\u7F6E
setup.proxy.host=\u4E3B\u673A
setup.proxy.port=\u7AEF\u53E3
setup.proxy.test=\u6D4B\u8BD5
proxy.test.header=\u8BF7\u8F93\u5165\u60A8\u8981\u68C0\u67E5\u7684\u4EFB\u4F55URL\uFF1A
proxy.test.title=\u68C0\u67E5\u4EE3\u7406\u8BBE\u7F6E
proxy.test.result.success=\u8FDE\u63A5\u6210\u529F
proxy.test.result.failed=\u8FDE\u63A5\u95EE\u9898:
about.alist.version=AList \u7248\u672C
about.app.version=GUI \u7248\u672C
about.alist.update=\u68C0\u67E5 AList \u7248\u672C
about.app.update=\u68C0\u67E5 GUI \u7248\u672C
setup.theme=\u4E3B\u9898
update.current=\u5F53\u524D\u7248\u672C
update.remote=\u6700\u65B0\u7248\u672C
update.upgrade.not=\u5DF2\u662F\u6700\u65B0\u7248\u672C
update.upgrade.new=\u68C0\u67E5\u5230\u65B0\u7248\u672C
msg.alist.download.notfile=\u6CA1\u68C0\u6D4B\u5230AList\u6587\u4EF6\uFF0C\u662F\u5426\u73B0\u5728\u4E0B\u8F7D\uFF1F
msg.alist.pwd.copy=\u590D\u5236\u6210\u529F
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

View File

@ -0,0 +1,53 @@
label.cancel=Cancel
root.tab.main=Home
root.tab.setup=Setup
root.tab.about=About
root.foot.doc=Document
root.foot.github=GitHub
root.foot.sponsor=Sponsor AList
main.control.start=Start
main.control.stop=Stop
main.control.password=Password
main.control.restart=Restart
main.control.more=More
main.status.label-running=Running
main.status.label-stop=Stoped
main.more.browser=Open in browser
main.more.open-config=Open configuration file
main.more.open-log=Open the log folder
alist.status.start=Starting AList
alist.status.start.running=AList is running...
alist.status.stop=Stopping AList
alist.status.stop.stopped=AList has stopped
setup.proxy=HTTP PROXY
setup.auto-start.label=Auto start with PC
setup.silent-startup.label=Silent startup
setup.language=language
proxy.setup.label.no_proxy=No Proxy
proxy.setup.label.system=System Proxy
proxy.setup.label.manual=Manual Config
setup.proxy.host=Host
setup.proxy.port=Port
setup.proxy.test=Check connection
proxy.test.header=Enter any URL to check connection to:
proxy.test.title=Check Proxy Settings
proxy.test.result.success=Connection successful
proxy.test.result.failed=Problem with connection:
about.alist.version=AList Version
about.app.version=GUI Version
about.alist.update=Check AList Version
about.app.update=Check GUI Version
setup.theme=Theme
update.current=Current Version
update.remote=Latest Version
update.upgrade.not=It is already the latest version
update.upgrade.new=Detected a new version
msg.alist.download.notfile=AList file not detected, download now?
msg.alist.pwd.copy=Copy successful
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

View File

@ -0,0 +1,53 @@
label.cancel=\u53D6\u6D88
root.tab.main=\u4E3B\u9875
root.tab.setup=\u8BBE\u7F6E
root.tab.about=\u5173\u4E8E
root.foot.doc=\u6587\u6863
root.foot.github=GitHub
root.foot.sponsor=\u8D5E\u52A9 AList
main.control.start=\u5F00\u59CB
main.control.stop=\u505C\u6B62
main.control.password=\u5BC6\u7801
main.control.restart=\u91CD\u542F
main.control.more=\u66F4\u591A
main.status.label-running=\u8FD0\u884C\u4E2D
main.status.label-stop=\u5DF2\u505C\u6B62
main.more.browser=\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00
main.more.open-config=\u6253\u5F00\u914D\u7F6E\u6587\u4EF6
main.more.open-log=\u6253\u5F00\u65E5\u5FD7\u6587\u4EF6\u5939
alist.status.stop.stopped=AList \u5DF2\u505C\u6B62
alist.status.start=\u6B63\u5728\u542F\u52A8 AList
alist.status.stop=\u6B63\u5728\u505C\u6B62AList
alist.status.start.running=AList \u6B63\u5728\u8FD0\u884C\u3002\u3002\u3002
setup.proxy=HTTP\u4EE3\u7406
setup.auto-start.label=\u5F00\u673A\u81EA\u542F
setup.silent-startup.label=\u9759\u9ED8\u542F\u52A8
setup.language=\u8BED\u8A00
proxy.setup.label.no_proxy=\u4E0D\u4EE3\u7406
proxy.setup.label.system=\u7CFB\u7EDF\u4EE3\u7406
proxy.setup.label.manual=\u624B\u52A8\u8BBE\u7F6E
setup.proxy.host=\u4E3B\u673A
setup.proxy.port=\u7AEF\u53E3
setup.proxy.test=\u6D4B\u8BD5
proxy.test.header=\u8BF7\u8F93\u5165\u60A8\u8981\u68C0\u67E5\u7684\u4EFB\u4F55URL\uFF1A
proxy.test.title=\u68C0\u67E5\u4EE3\u7406\u8BBE\u7F6E
proxy.test.result.success=\u8FDE\u63A5\u6210\u529F
proxy.test.result.failed=\u8FDE\u63A5\u95EE\u9898:
about.alist.version=AList \u7248\u672C
about.app.version=GUI \u7248\u672C
about.alist.update=\u68C0\u67E5 AList \u7248\u672C
about.app.update=\u68C0\u67E5 GUI \u7248\u672C
setup.theme=\u4E3B\u9898
update.current=\u5F53\u524D\u7248\u672C
update.remote=\u6700\u65B0\u7248\u672C
update.upgrade.not=\u5DF2\u662F\u6700\u65B0\u7248\u672C
update.upgrade.new=\u68C0\u67E5\u5230\u65B0\u7248\u672C
msg.alist.download.notfile=\u6CA1\u68C0\u6D4B\u5230AList\u6587\u4EF6\uFF0C\u662F\u5426\u73B0\u5728\u4E0B\u8F7D\uFF1F
msg.alist.pwd.copy=\u590D\u5236\u6210\u529F
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

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="logback.logdir" value="log"/>
<property name="CHARSET" value="utf-8"/>
<property name="logback.app" value="alist-gui"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%highlight(%d{YYYY:MM:dd HH:mm:ss.SSS}) ${logback.app} %boldYellow([%thread]) %highlight(%-5level) %cyan(%logger{36}) - %mdc{client} [%X{trace_id}] %msg%n"/>
<!--输出到控制台 ConsoleAppender-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${CHARSET}</charset>
</encoder>
</appender>
<!--输出到文件 fileLog-->
<appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"/>
<File>${logback.logdir}/${logback.app}.info.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.info.log</FileNamePattern>
<!--只保留最近30天的日志-->
<maxHistory>30</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>${CHARSET}</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n</pattern>
</encoder>
<!--只打印错误日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="fileLog-debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"/>
<File>${logback.logdir}/${logback.app}.debug.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.debug.log</FileNamePattern>
<!--只保留最近30天的日志-->
<maxHistory>30</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n</pattern>
</encoder>
<!--只打印错误日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 错误日志 -->
<appender name="fileLog-err" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${logback.logdir}/${logback.app}.err.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${logback.logdir}/${logback.app}_%d{yyyy-MM-dd}.err.log</FileNamePattern>
<maxHistory>7</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %mdc{client} [%X{trace_id}] %msg%n</pattern>
</encoder>
<!--只打印错误日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--指定最基础的日志输出级别-->
<root level="INFO">
<!--appender将会添加到这个logger-->
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
<appender-ref ref="fileLog-debug"/>
<appender-ref ref="fileLog-err"/>
</root>
</configuration>