├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── encodings.xml
├── gradle.xml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── jarRepositories.xml
├── kotlinc.xml
├── misc.xml
├── modules.xml
├── modules
│ └── FileSyncUtils.test.iml
├── uiDesigner.xml
└── vcs.xml
├── Docs.md
├── Docs
└── 1.png
├── LICENSE
├── README.MD
├── build.gradle
├── exe4j_configuration.exe4j
├── filechangelistener.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── main.gif
├── multiserver.gif
├── ruleeditor.gif
├── settings.gradle
└── src
└── main
├── java
└── github
│ └── kasuminova
│ ├── balloonserver
│ ├── BalloonServer.java
│ ├── configurations
│ │ ├── BalloonServerConfig.java
│ │ ├── CloseOperation.java
│ │ ├── Configuration.java
│ │ ├── ConfigurationManager.java
│ │ ├── IntegratedServerConfig.java
│ │ └── RemoteClientConfig.java
│ ├── gui
│ │ ├── ConfirmExitDialog.java
│ │ ├── SetupSwing.java
│ │ ├── SmoothProgressBar.java
│ │ ├── SwingSystemTray.java
│ │ ├── checkboxtree
│ │ │ ├── CheckBoxTreeCellRenderer.java
│ │ │ ├── CheckBoxTreeLabel.java
│ │ │ ├── CheckBoxTreeNode.java
│ │ │ └── CheckBoxTreeNodeSelectionListener.java
│ │ ├── fileobjectbrowser
│ │ │ ├── FileObjectBrowser.java
│ │ │ └── ImageListCellRenderer.java
│ │ ├── layoutmanager
│ │ │ └── VFlowLayout.java
│ │ ├── panels
│ │ │ ├── AboutPanel.java
│ │ │ └── SettingsPanel.java
│ │ └── ruleeditor
│ │ │ ├── RuleEditor.java
│ │ │ └── RuleEditorActionListener.java
│ ├── httpserver
│ │ ├── ContentRanges.java
│ │ ├── DecodeProxy.java
│ │ ├── HttpRequestHandler.java
│ │ ├── HttpServer.java
│ │ ├── HttpServerInitializer.java
│ │ ├── HttpServerInterface.java
│ │ └── SslContextFactoryOne.java
│ ├── remoteclient
│ │ ├── AbstractRemoteClientChannel.java
│ │ ├── LastChannel.java
│ │ ├── RemoteClient.java
│ │ ├── RemoteClientChannel.java
│ │ ├── RemoteClientFileChannel.java
│ │ └── RemoteClientInitializer.java
│ ├── servers
│ │ ├── AbstractGUIServer.java
│ │ ├── AbstractServer.java
│ │ ├── GUIServerInterface.java
│ │ ├── LogPaneMouseAdapter.java
│ │ ├── ServerInterface.java
│ │ ├── localserver
│ │ │ ├── AddUpdateRule.java
│ │ │ ├── DeleteUpdateRule.java
│ │ │ ├── IntegratedServer.java
│ │ │ ├── IntegratedServerInterface.java
│ │ │ └── ShowOrHideComponentActionListener.java
│ │ └── remoteserver
│ │ │ ├── RemoteClientInterface.java
│ │ │ └── RemoteIntegratedServerClient.java
│ ├── updatechecker
│ │ ├── ApplicationVersion.java
│ │ ├── Checker.java
│ │ └── HttpClient.java
│ └── utils
│ │ ├── BatchUtils.java
│ │ ├── CustomThreadFactory.java
│ │ ├── FileUtil.java
│ │ ├── GUILogger.java
│ │ ├── HashCalculator.java
│ │ ├── HashStrings.java
│ │ ├── IPAddressUtil.java
│ │ ├── MiscUtils.java
│ │ ├── ModernColors.java
│ │ ├── NextFileListener.java
│ │ ├── NextHashCalculator.java
│ │ ├── Security.java
│ │ ├── SvgIcons.java
│ │ ├── filecacheutils
│ │ ├── DirSizeCalculatorThread.java
│ │ ├── FileCacheCalculator.java
│ │ ├── JsonCacheCheckerTask.java
│ │ └── JsonCacheUtils.java
│ │ └── fileobject
│ │ ├── AbstractSimpleFileObject.java
│ │ ├── DirInfoTask.java
│ │ ├── FileInfoTask.java
│ │ ├── SimpleDirectoryObject.java
│ │ └── SimpleFileObject.java
│ └── messages
│ ├── AbstractMessage.java
│ ├── AuthSuccessMessage.java
│ ├── FileListMessage.java
│ ├── LogMessage.java
│ ├── MessageProcessor.java
│ ├── MethodMessage.java
│ ├── RequestMessage.java
│ ├── StatusMessage.java
│ ├── TokenMessage.java
│ ├── filemessages
│ ├── FileInfoMsg.java
│ ├── FileMessage.java
│ ├── FileObjMessage.java
│ └── FileRequestMsg.java
│ └── processor
│ └── MessageProcessor.java
└── resources
├── font
├── HarmonyOS_Sans_SC+JetBrains_Mono.ttf
├── HarmonyOS_Sans_SC+Saira.ttf
├── HarmonyOS_Sans_SC_LICENSE.txt
├── JetBrains_Mono_OFL.txt
└── Saira_OFL.txt
├── icons
├── custom_server.svg
├── default_server.svg
├── delete.svg
├── edit.svg
├── file_types
│ ├── class.svg
│ ├── dir.svg
│ ├── doc_docx.svg
│ ├── exe.svg
│ ├── file_default.svg
│ ├── jar.svg
│ ├── java.svg
│ ├── jpg.svg
│ ├── json.svg
│ ├── md.svg
│ ├── ppt_pptx.svg
│ ├── txt.svg
│ ├── xls_xlsx.svg
│ ├── xml.svg
│ ├── yml.svg
│ └── zip.svg
├── info.svg
├── play.svg
├── plus.svg
├── reload.svg
├── remove.svg
├── resource.svg
├── serverList.svg
├── settings.svg
├── stop.svg
└── terminal.svg
└── image
├── icon_16x16.ico
├── icon_16x16.png
└── splash.png
/.gitignore:
--------------------------------------------------------------------------------
1 | /src/test/
2 | /res/
3 | *.json
4 | /build
5 | /Test
6 | /java17-jre
7 | /out
8 | /.gradle
9 | /logs
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # 默认忽略的文件
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/modules/FileSyncUtils.test.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Docs.md:
--------------------------------------------------------------------------------
1 | # BalloonServer 服务端 Manual
2 | BalloonServer 是 LittleServer 的衍生图形化服务端,并且底层基于高性能的 Netty-IO,性能更佳。
3 | ## 优点
4 | - 完全可视化操作,上手简单便捷
5 | - 多实例化,可以同时载入多个服务器
6 | - 开箱即用,支持双击启动和 Shell 启动
7 | - 支持配置热重载
8 | - 支持 SSL 证书
9 | - 支持实时文件监听
10 | - 支持全自动更新服务端(仅 EXE 版本),实现全自动服务端维护
11 | - 支持最小化到任务栏托盘(需要系统支持)
12 | - 跨平台(Linux, Windows, MacOS)
13 | - 高性能多线程处理,最大化利用服务器资源,减少卡顿
14 | ## 下载
15 | 你可以在 [GitHub Release](https://github.com/BalloonUpdate/BalloonServer/releases) 或在我们的 [官方群聊](https://jq.qq.com/?_wv=1027&k=bhNBCnUQ) 内找到本软件的发行版。
16 |
17 | **注意:从 1.0.6-BETA 版本起,程序的最低 JAVA 版本要求提高到了 17。**
18 |
19 | 下载程序后,双击 JAR 或执行命令 `java -jar BalloonServer-1.x.x-BETA.jar` 即可启动程序。
20 |
21 | ## 窗口介绍
22 | 
23 |
24 | - 最大的窗口为`服务器实例日志`窗口,这里将会输出服务器的相关日志。
25 | - `控制面板`是每个服务端实例的配置界面,这里将会是你后期最经常接触的面板。
26 | - `集成服务端`标签页是本程序的主服务端,而`旧版集成服务端`是为了兼容旧版客户端而生的服务端。
27 | - `上传列表`会展示出当前正常向客户端发送的文件列表和进度。
28 | - 最上方菜单栏为实例管理菜单,用于 创建/管理 自定义服务器实例,通常情况下,大部分用户只需要使用主服务端即可。
29 | ***提示:如果操作系统支持系统托盘,则关闭窗口的时候不会关闭程序,而是会最小化到任务栏。***
30 | ***左击托盘图标即可打开程序,右击托盘图标可打开菜单以退出程序。***
31 |
32 | ### 控制面板
33 | 控制面板将会是你后期最经常接触的面板,如果你不知道这些配置的含义,**请仔细阅读下方内容。**
34 | #### 监听 IP, 端口
35 | 点击 `重载配置并启动服务器` 按钮时,程序将会监听此 `IP` 指定的`端口`的的传入请求。**如果你不知道这些内容的含义,请不要动它。**
36 | #### 资源文件夹
37 | 程序将会 扫描/监听 的文件夹,默认为 `/res`,**如果无特殊需求,请不要动它。**
38 | - 资源文件夹是所有客户端的入口,以如果玩家访问除 `res.json` `index.json` `/res` 之外的路径,将会返回 403 错误。
39 | - 例如如果你需要更新客户端模组,请复制 Minecraft 客户端中所有的模组文件到 `res/.minecraft/mods/` 里(内部目录请自行创建),注意是所有文件。如果你要更新其它文件,同样按上面的方法,复制到 `/res` 目录里对应的路径的目录上(比如 `vexview` 的贴图复制到 `/res/.minecraft/vexview/textures/` 下,其它文件同理)
40 | #### JKS 证书文件
41 | 用于 `HTTPS` 验证所需要的文件,如果没有 `JKS 证书`,则服务器默认使用 `HTTP` 协议与客户端传输。
42 | - `JKS 证书`即为后缀名为 `.jks` 的文件,点击输入框右方的 `选择` 按钮即可选择证书,证书可以在任何路径
43 | #### JKS 证书密码
44 | 用于 `HTTPS` 验证所需要的文件,如果没有 `JKS 证书`,则服务器默认使用 `HTTP` 协议与客户端传输。
45 |
46 | - `JKS 证书密码`是用于验证完整性的密钥,如果没有它,即使拥有 `JKS 证书文件` 也无法正常使用 `HTTPS` 协议。
47 |
48 | #### 实时文件监听
49 | 此选项开启后,启动服务器的同时会启动文件监听服务。
50 |
51 | 文件监听服务会每隔 5 - 7 秒会统计一次资源文件夹的变化,如果资源一有变化就会**立即**重新生成资源缓存。
52 |
53 | 此功能使用最小化更新模式的方法生成缓存,并且**不需要**重启服务端。
54 |
55 | 适合在频繁变动文件的情况下使用此功能。
56 |
57 | #### 普通更新模式 补全更新模式
58 | `普通更新模式` :客户端从服务器获取信息时,在此列表内的匹配的 `文件 / 文件夹` **都将会被更新**,规则可以是`正则表达式`、`Glob 表达式`
59 |
60 | `补全更新模式` : 客户端从服务器获取信息时,只会在首次 `文件 / 文件夹` **不存在** 时会进行一次下载,如果后续文件存在,就会跳过更新**不会覆盖**已有内容。一般用来补全一些配置文件,规则可以是`正则表达式`、`Glob 表达式`。
61 |
62 | - 要新建一个更新规则,请在对应的列表内右击,然后在弹出的菜单内点击 `添加更新规则`,然后在弹出的对话框内输入更新规则。
63 | - 要删除一个更新规则,请先选中一个要删除的更新规则,然后右击,接着在弹出的菜单内点击 `删除更新规则`。
64 | - [一些更新规则示例](https://github.com/BalloonUpdate/Docs/blob/old-servers/server/reference.md)
65 |
66 | #### 重载配置
67 | 点击后,服务端会将当前的程序配置应用到程序内,但是不会应用到服务器内。
68 |
69 | #### 保存配置并重载
70 | 点击后,服务端会将当前的程序配置应用到程序内,并保存当前的配置文件至磁盘,但是不会应用到服务器内。
71 |
72 | #### 重新生成资源文件夹缓存
73 | 点击后,服务端会主动生成资源文件夹的缓存,并保存至磁盘。并且会重载服务器的资源文件夹缓存。
74 | ***即使服务器正在运行,程序也可以重载服务器的缓存***
75 |
76 | #### 重载配置并启动服务器
77 | 点击后,服务端会将当前的程序配置应用到程序内,并应用到服务器内,然后生成资源文件夹缓存,最后启动服务器。如果启用了 `实时文件监听` 功能,程序还会启动实时文件监听器。
78 |
79 | #### 关闭服务器
80 | 点击后,服务端将会在完成最后任务后停止监听端口,如果启用了 `实时文件监听` 功能,程序也会关闭实时文件监听器。
81 |
82 | *Enjoy it~*
83 |
--------------------------------------------------------------------------------
/Docs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/Docs/1.png
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | ## BalloonServer
2 | BalloonServer 是 LittleServer 的完全图形化版本,基于 Netty-IO 的增强实现。
3 |
4 | [程序使用文档](./Docs.md)
5 |
6 | ### 程序运行实例:
7 |
8 | #### 主功能
9 | 
10 |
11 | #### 可视化更新规则编辑器
12 | 
13 |
14 | #### 实时文件监听器
15 | 
16 |
17 | #### 多服务端实例
18 | 
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | }
4 |
5 | group 'github.kasuminova'
6 | version '1.4.0-BETA'
7 |
8 | repositories {
9 | maven {
10 | url 'https://maven.aliyun.com/nexus/content/groups/public/'
11 | }
12 | maven {
13 | url 'https://maven.aliyun.com/repository/central'
14 | }
15 | mavenCentral()
16 | }
17 |
18 | jar {
19 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE
20 | archivesBaseName = 'BalloonServer-GUI'//基本的文件名
21 | archiveVersion = version //版本
22 | manifest { //配置 Jar 文件的 Manifest
23 | attributes (
24 | "Manifest-Version": 1.0,
25 | 'Main-Class': 'github.kasuminova.balloonserver.BalloonServer', //指定 Main 方法所在的文件
26 | 'SplashScreen-Image': 'image/splash.png'
27 | )
28 | }
29 |
30 | //打包依赖包
31 | from {
32 | (configurations.runtimeClasspath).collect {
33 | it.isDirectory() ? it : zipTree(it)
34 | }
35 | }
36 | }
37 |
38 | dependencies {
39 | testImplementation 'org.projectlombok:lombok:1.18.24'
40 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
41 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
42 |
43 | implementation 'com.alibaba.fastjson2:fastjson2:2.0.19'
44 |
45 | implementation 'io.netty:netty-buffer:4.1.85.Final'
46 | implementation 'io.netty:netty-codec:4.1.85.Final'
47 | implementation 'io.netty:netty-codec-http:4.1.85.Final'
48 | implementation 'io.netty:netty-handler:4.1.85.Final'
49 | implementation 'io.netty:netty-transport:4.1.85.Final'
50 |
51 | implementation 'com.formdev:flatlaf:2.6'
52 | implementation 'com.formdev:flatlaf-extras:2.6'
53 | implementation 'com.formdev:flatlaf-intellij-themes:2.6'
54 |
55 | implementation 'cn.hutool:hutool-core:5.8.9'
56 | implementation 'cn.hutool:hutool-system:5.8.9'
57 | implementation 'cn.hutool:hutool-log:5.8.9'
58 | implementation 'cn.hutool:hutool-http:5.8.9'
59 | }
60 |
61 | tasks.withType(JavaCompile) {
62 | options.encoding = "UTF-8"
63 | // options.compilerArgs += "--enable-preview"
64 | }
65 |
66 | test {
67 | useJUnitPlatform()
68 | }
--------------------------------------------------------------------------------
/exe4j_configuration.exe4j:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/filechangelistener.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/filechangelistener.gif
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-milestone-1-bin.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/main.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/main.gif
--------------------------------------------------------------------------------
/multiserver.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/multiserver.gif
--------------------------------------------------------------------------------
/ruleeditor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/ruleeditor.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'BalloonServer'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/configurations/BalloonServerConfig.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.configurations;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 |
5 | public class BalloonServerConfig extends Configuration {
6 | public static final CloseOperation QUERY = new CloseOperation(0, "每次询问");
7 | public static final CloseOperation HIDE_ON_CLOSE = new CloseOperation(1, "最小化至托盘");
8 | public static final CloseOperation EXIT_ON_CLOSE = new CloseOperation(2, "退出程序");
9 | /**
10 | * 自动启动服务器
11 | */
12 | @JSONField(ordinal = 1)
13 | private boolean autoStartServer = false;
14 | /**
15 | * 自动启动服务器(仅一次)
16 | */
17 | @JSONField(ordinal = 2)
18 | private boolean autoStartServerOnce = false;
19 | /**
20 | * 自动检查更新
21 | */
22 | @JSONField(ordinal = 3)
23 | private boolean autoCheckUpdates = false;
24 | /**
25 | * 自动更新
26 | */
27 | @JSONField(ordinal = 4)
28 | private boolean autoUpdate = false;
29 | /**
30 | * 关闭窗口的操作
31 | */
32 | @JSONField(ordinal = 5)
33 | private int closeOperation = 0;
34 | /**
35 | * 单线程模式
36 | */
37 | @JSONField(ordinal = 6)
38 | private boolean singleThreadMode = false;
39 | /**
40 | * 文件线程池大小
41 | */
42 | @JSONField(ordinal = 7)
43 | private int fileThreadPoolSize = 0;
44 | /**
45 | * DEBUG 模式
46 | */
47 | @JSONField(ordinal = 8)
48 | private boolean debugMode = false;
49 |
50 | public BalloonServerConfig() {
51 | configVersion = 0;
52 | }
53 |
54 | @Override
55 | public BalloonServerConfig setConfigVersion(int configVersion) {
56 | this.configVersion = configVersion;
57 | return this;
58 | }
59 |
60 | public boolean isDebugMode() {
61 | return debugMode;
62 | }
63 |
64 | public BalloonServerConfig setDebugMode(boolean debugMode) {
65 | this.debugMode = debugMode;
66 | return this;
67 | }
68 |
69 | public boolean isAutoStartServer() {
70 | return autoStartServer;
71 | }
72 |
73 | public BalloonServerConfig setAutoStartServer(boolean autoStartServer) {
74 | this.autoStartServer = autoStartServer;
75 | return this;
76 | }
77 |
78 | public boolean isAutoStartServerOnce() {
79 | return autoStartServerOnce;
80 | }
81 |
82 | public BalloonServerConfig setAutoStartServerOnce(boolean autoStartServerOnce) {
83 | this.autoStartServerOnce = autoStartServerOnce;
84 | return this;
85 | }
86 |
87 | public int getCloseOperation() {
88 | return closeOperation;
89 | }
90 |
91 | public BalloonServerConfig setCloseOperation(int closeOperation) {
92 | this.closeOperation = closeOperation;
93 | return this;
94 | }
95 |
96 | public boolean isAutoUpdate() {
97 | return autoUpdate;
98 | }
99 |
100 | public BalloonServerConfig setAutoUpdate(boolean autoUpdate) {
101 | this.autoUpdate = autoUpdate;
102 | return this;
103 | }
104 |
105 | public boolean isAutoCheckUpdates() {
106 | return autoCheckUpdates;
107 | }
108 |
109 | public BalloonServerConfig setAutoCheckUpdates(boolean autoCheckUpdates) {
110 | this.autoCheckUpdates = autoCheckUpdates;
111 | return this;
112 | }
113 |
114 | public boolean isSingleThreadMode() {
115 | return singleThreadMode;
116 | }
117 |
118 | public BalloonServerConfig setSingleThreadMode(boolean singleThreadMode) {
119 | this.singleThreadMode = singleThreadMode;
120 | return this;
121 | }
122 |
123 | public int getFileThreadPoolSize() {
124 | return fileThreadPoolSize;
125 | }
126 |
127 | public BalloonServerConfig setFileThreadPoolSize(int fileThreadPoolSize) {
128 | this.fileThreadPoolSize = fileThreadPoolSize;
129 | return this;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/configurations/CloseOperation.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.configurations;
2 |
3 | public class CloseOperation {
4 | private final int operation;
5 | private String desc;
6 |
7 | public CloseOperation(int operation, String desc) {
8 | this.operation = operation;
9 | this.desc = desc;
10 | }
11 |
12 | public int getOperation() {
13 | return operation;
14 | }
15 |
16 | public String getDesc() {
17 | return desc;
18 | }
19 |
20 | public void setDesc(String desc) {
21 | this.desc = desc;
22 | }
23 |
24 | @Override
25 | public String toString() {
26 | return desc;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/configurations/Configuration.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.configurations;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 |
5 | /**
6 | * 配置文件抽象类
7 | */
8 | abstract class Configuration {
9 | public static final int DEFAULT_PORT = 8080;
10 |
11 | @JSONField(ordinal = 100)
12 | public int configVersion;
13 |
14 | public int getConfigVersion() {
15 | return configVersion;
16 | }
17 |
18 | /**
19 | * 设置配置文件版本
20 | * @param configVersion 版本
21 | * @return Configuration
22 | */
23 | public abstract Configuration setConfigVersion(int configVersion);
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/configurations/ConfigurationManager.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.configurations;
2 |
3 | import cn.hutool.core.io.IORuntimeException;
4 | import com.alibaba.fastjson2.JSON;
5 | import com.alibaba.fastjson2.JSONObject;
6 | import com.alibaba.fastjson2.JSONWriter;
7 | import github.kasuminova.balloonserver.utils.FileUtil;
8 |
9 | import java.io.IOException;
10 | import java.nio.file.Files;
11 | import java.nio.file.Paths;
12 |
13 | /**
14 | * @author Kasumi_Nova
15 | */
16 | public class ConfigurationManager {
17 | public static void loadLittleServerConfigFromFile(String path, IntegratedServerConfig oldConfig) throws IOException {
18 | IntegratedServerConfig newConfig = JSON.parseObject(Files.newInputStream(Paths.get(path)), IntegratedServerConfig.class);
19 |
20 | oldConfig.setConfigVersion(newConfig.getConfigVersion())
21 | .setIp(newConfig.getIp())
22 | .setPort(newConfig.getPort())
23 | .setMainDirPath(newConfig.getMainDirPath())
24 | .setFileChangeListener(newConfig.isFileChangeListener())
25 | .setCompatibleMode(newConfig.isCompatibleMode())
26 | .setJksFilePath(newConfig.getJksFilePath())
27 | .setJksSslPassword(newConfig.getJksSslPassword())
28 | .setCommonMode(newConfig.getCommonMode())
29 | .setOnceMode(newConfig.getOnceMode());
30 | }
31 |
32 | public static void loadBalloonServerConfigFromFile(String path, BalloonServerConfig oldConfig) throws IOException {
33 | BalloonServerConfig config = JSON.parseObject(Files.newInputStream(Paths.get(path)), BalloonServerConfig.class);
34 | oldConfig.setAutoStartServer(config.isAutoStartServer())
35 | .setAutoStartServerOnce(config.isAutoStartServerOnce())
36 | .setDebugMode(config.isDebugMode())
37 | .setCloseOperation(config.getCloseOperation())
38 | .setAutoCheckUpdates(config.isAutoCheckUpdates())
39 | .setAutoUpdate(config.isAutoUpdate())
40 | .setSingleThreadMode(config.isSingleThreadMode())
41 | .setFileThreadPoolSize(config.getFileThreadPoolSize());
42 | }
43 |
44 | public static void loadRemoteClientConfigFromFile(String path, RemoteClientConfig oldConfig) throws IOException {
45 | RemoteClientConfig config = JSON.parseObject(Files.newInputStream(Paths.get(path)), RemoteClientConfig.class);
46 | oldConfig.setToken(config.getToken())
47 | .setIp(config.getIp())
48 | .setPort(config.getPort());
49 | }
50 |
51 | public static void saveConfigurationToFile(Configuration configuration, String path, String name) throws IORuntimeException {
52 | FileUtil.createJsonFile(JSONObject.toJSONString(configuration, JSONWriter.Feature.PrettyFormat), path, name);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/configurations/IntegratedServerConfig.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.configurations;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 |
5 | import java.io.Serial;
6 | import java.io.Serializable;
7 |
8 | /**
9 | * @author Kasumi_Nova
10 | */
11 | public class IntegratedServerConfig extends Configuration implements Serializable {
12 | @Serial
13 | private static final long serialVersionUID = 1L;
14 |
15 | public static final String DEFAULT_IP = "127.0.0.1";
16 | public static final String DEFAULT_MAIN_DIR_PATH = "/res";
17 | public static final boolean DEFAULT_FILE_CHANGE_LISTENER = true;
18 | public static final boolean DEFAULT_COMPATIBLE_MODE = false;
19 |
20 | @JSONField(ordinal = 1)
21 | private String ip = DEFAULT_IP;
22 | @JSONField(ordinal = 2)
23 | private int port = DEFAULT_PORT;
24 | @JSONField(ordinal = 3)
25 | private String mainDirPath = DEFAULT_MAIN_DIR_PATH;
26 | @JSONField(ordinal = 4)
27 | private boolean fileChangeListener = DEFAULT_FILE_CHANGE_LISTENER;
28 | @JSONField(ordinal = 5)
29 | private boolean compatibleMode = DEFAULT_COMPATIBLE_MODE;
30 | @JSONField(ordinal = 6)
31 | private String jksFilePath = "";
32 | @JSONField(ordinal = 7)
33 | private String jksSslPassword = "";
34 | @JSONField(ordinal = 8)
35 | private String[] commonMode = new String[0];
36 | @JSONField(ordinal = 9)
37 | private String[] onceMode = new String[0];
38 |
39 | public IntegratedServerConfig() {
40 | configVersion = 1;
41 | }
42 |
43 | /**
44 | * 重置配置文件
45 | */
46 | public IntegratedServerConfig reset() {
47 | configVersion = 1;
48 | ip = "127.0.0.1";
49 | port = DEFAULT_PORT;
50 | mainDirPath = "/res";
51 | fileChangeListener = true;
52 | jksFilePath = "";
53 | jksSslPassword = "";
54 | commonMode = new String[0];
55 | onceMode = new String[0];
56 | return this;
57 | }
58 |
59 | @Override
60 | public IntegratedServerConfig setConfigVersion(int configVersion) {
61 | this.configVersion = configVersion;
62 | return this;
63 | }
64 |
65 | public String getIp() {
66 | return ip;
67 | }
68 |
69 | public IntegratedServerConfig setIp(String ip) {
70 | this.ip = ip;
71 | return this;
72 | }
73 |
74 | public int getPort() {
75 | return port;
76 | }
77 |
78 | public IntegratedServerConfig setPort(int port) {
79 | this.port = port;
80 | return this;
81 | }
82 |
83 | public String getJksFilePath() {
84 | return jksFilePath;
85 | }
86 |
87 | public IntegratedServerConfig setJksFilePath(String jksFilePath) {
88 | this.jksFilePath = jksFilePath;
89 | return this;
90 | }
91 |
92 | public String getJksSslPassword() {
93 | return jksSslPassword;
94 | }
95 |
96 | public IntegratedServerConfig setJksSslPassword(String jksSslPassword) {
97 | this.jksSslPassword = jksSslPassword;
98 | return this;
99 | }
100 |
101 | public String[] getCommonMode() {
102 | return commonMode;
103 | }
104 |
105 | public IntegratedServerConfig setCommonMode(String[] commonMode) {
106 | this.commonMode = commonMode;
107 | return this;
108 | }
109 |
110 | public String[] getOnceMode() {
111 | return onceMode;
112 | }
113 |
114 | public IntegratedServerConfig setOnceMode(String[] onceMode) {
115 | this.onceMode = onceMode;
116 | return this;
117 | }
118 |
119 | public String getMainDirPath() {
120 | return mainDirPath;
121 | }
122 |
123 | public IntegratedServerConfig setMainDirPath(String mainDirPath) {
124 | this.mainDirPath = mainDirPath;
125 | return this;
126 | }
127 |
128 | public boolean isFileChangeListener() {
129 | return fileChangeListener;
130 | }
131 |
132 | public IntegratedServerConfig setFileChangeListener(boolean fileChangeListener) {
133 | this.fileChangeListener = fileChangeListener;
134 | return this;
135 | }
136 |
137 | public boolean isCompatibleMode() {
138 | return compatibleMode;
139 | }
140 |
141 | public IntegratedServerConfig setCompatibleMode(boolean compatibleMode) {
142 | this.compatibleMode = compatibleMode;
143 | return this;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/configurations/RemoteClientConfig.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.configurations;
2 |
3 | import com.alibaba.fastjson2.annotation.JSONField;
4 |
5 | /**
6 | * @author Kasumi_Nova
7 | */
8 | public class RemoteClientConfig extends Configuration {
9 | @JSONField(ordinal = 1)
10 | private String ip = "127.0.0.1";
11 | @JSONField(ordinal = 2)
12 | private int port = 8080;
13 | @JSONField(ordinal = 3)
14 | private String token = "";
15 |
16 | public RemoteClientConfig() {
17 | this.configVersion = 0;
18 | }
19 |
20 | @Override
21 | public RemoteClientConfig setConfigVersion(int configVersion) {
22 | this.configVersion = configVersion;
23 | return this;
24 | }
25 |
26 | public String getIp() {
27 | return ip;
28 | }
29 |
30 | public RemoteClientConfig setIp(String ip) {
31 | this.ip = ip;
32 | return this;
33 | }
34 |
35 | public int getPort() {
36 | return port;
37 | }
38 |
39 | public RemoteClientConfig setPort(int port) {
40 | this.port = port;
41 | return this;
42 | }
43 |
44 | public String getToken() {
45 | return token;
46 | }
47 |
48 | public RemoteClientConfig setToken(String token) {
49 | this.token = token;
50 | return this;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/ConfirmExitDialog.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui;
2 |
3 | import github.kasuminova.balloonserver.BalloonServer;
4 | import github.kasuminova.balloonserver.configurations.BalloonServerConfig;
5 | import github.kasuminova.balloonserver.configurations.ConfigurationManager;
6 | import github.kasuminova.balloonserver.gui.layoutmanager.VFlowLayout;
7 |
8 | import javax.swing.*;
9 | import javax.swing.border.EmptyBorder;
10 | import java.awt.*;
11 | import java.awt.event.WindowAdapter;
12 | import java.awt.event.WindowEvent;
13 |
14 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_LOGGER;
15 | import static github.kasuminova.balloonserver.BalloonServer.stopAllServers;
16 |
17 | /**
18 | * @author Kasumi_Nova
19 | */
20 | public class ConfirmExitDialog extends JDialog {
21 |
22 | public static final int DIALOG_WIDTH = 360;
23 | public static final int DIALOG_HEIGHT = 165;
24 |
25 | public ConfirmExitDialog(JFrame frame, BalloonServerConfig config) {
26 | setTitle(BalloonServer.TITLE);
27 | setIconImage(BalloonServer.ICON.getImage());
28 | setSize(DIALOG_WIDTH, DIALOG_HEIGHT);
29 | setResizable(false);
30 | setLocationRelativeTo(null);
31 |
32 | JPanel contentPane = (JPanel) getContentPane();
33 | contentPane.setLayout(new VFlowLayout());
34 | contentPane.setBorder(new EmptyBorder(0, 10, 0, 10));
35 | contentPane.add(new JLabel("请选择点击关闭按钮时程序的操作:"));
36 |
37 | //选择 退出程序 或 最小化任务栏
38 | ButtonGroup selections = new ButtonGroup();
39 | JRadioButton miniSizeToTray = new JRadioButton("最小化到任务栏", true);
40 | JRadioButton exit = new JRadioButton("退出程序");
41 | selections.add(miniSizeToTray);
42 | selections.add(exit);
43 | JPanel radioButtonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5));
44 | radioButtonsPanel.add(miniSizeToTray);
45 | radioButtonsPanel.add(exit);
46 | contentPane.add(radioButtonsPanel);
47 |
48 | //始终保存选项
49 | JCheckBox saveSelection = new JCheckBox("保存选项, 下次不再提醒");
50 | saveSelection.setBorder(new EmptyBorder(0, 5, 0, 0));
51 | contentPane.add(saveSelection);
52 |
53 | Box buttonBox = new Box(BoxLayout.LINE_AXIS);
54 | buttonBox.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
55 |
56 | //确定取消按钮
57 | JButton yes = new JButton("确定");
58 | yes.addActionListener(e -> {
59 | //保存配置并退出程序
60 | if (exit.isSelected()) {
61 | //如果始终保存选项选中,则写入配置
62 | if (saveSelection.isSelected()) {
63 | config.setCloseOperation(BalloonServerConfig.EXIT_ON_CLOSE.getOperation());
64 | try {
65 | ConfigurationManager.saveConfigurationToFile(config, "./", "balloonserver");
66 | } catch (Exception ex) {
67 | GLOBAL_LOGGER.error("主程序配置文件保存失败!", ex);
68 | }
69 | }
70 | //停止所有正在运行的服务器并保存配置
71 | stopAllServers(true);
72 | System.exit(0);
73 | }
74 | //保存配置并最小化窗口
75 | if (miniSizeToTray.isSelected()) {
76 | frame.setVisible(false);
77 |
78 | //如果始终保存选项选中,则写入配置
79 | if (saveSelection.isSelected()) {
80 | frame.setDefaultCloseOperation(HIDE_ON_CLOSE);
81 | config.setCloseOperation(BalloonServerConfig.HIDE_ON_CLOSE.getOperation());
82 | try {
83 | ConfigurationManager.saveConfigurationToFile(config, "./", "balloonserver");
84 | } catch (Exception ex) {
85 | GLOBAL_LOGGER.error("主程序配置文件保存失败!", ex);
86 | }
87 | }
88 | }
89 |
90 | frame.setEnabled(true);
91 | dispose();
92 | });
93 | JButton cancel = new JButton("取消");
94 | cancel.addActionListener(e -> {
95 | frame.setEnabled(true);
96 | dispose();
97 | });
98 | addWindowListener(new WindowClosingAdapter(frame));
99 |
100 | buttonBox.add(cancel);
101 | buttonBox.add(new JLabel(" "));
102 | buttonBox.add(yes);
103 | contentPane.add(buttonBox);
104 | }
105 |
106 | private static class WindowClosingAdapter extends WindowAdapter {
107 | private final JFrame frame;
108 |
109 | private WindowClosingAdapter(JFrame frame) {
110 | this.frame = frame;
111 | }
112 |
113 | @Override
114 | public void windowClosing(WindowEvent e) {
115 | frame.setEnabled(true);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/SmoothProgressBar.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui;
2 |
3 | import cn.hutool.core.thread.ExecutorBuilder;
4 | import cn.hutool.core.thread.ThreadUtil;
5 | import github.kasuminova.balloonserver.utils.CustomThreadFactory;
6 |
7 | import javax.swing.*;
8 | import java.util.concurrent.LinkedBlockingQueue;
9 | import java.util.concurrent.ThreadPoolExecutor;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | /**
13 | * 平滑进度条
14 | */
15 | public class SmoothProgressBar extends JProgressBar {
16 | public static final long TIME_MULTIPLIER = 3L;
17 | //单线程线程池,用于保证进度条的操作顺序
18 | private final ThreadPoolExecutor singleThreadExecutor = ExecutorBuilder.create()
19 | .setCorePoolSize(1)
20 | .setMaxPoolSize(1)
21 | .setKeepAliveTime(10, TimeUnit.SECONDS)
22 | .setWorkQueue(new LinkedBlockingQueue<>())
23 | .setThreadFactory(CustomThreadFactory.create("SmoothProgressBarQueueThread-{}"))
24 | .build();
25 |
26 | private final int flowTime;
27 | //每秒刷新频率
28 | private final int frequency;
29 |
30 | /**
31 | * 创建一个平滑进度条,基于 JProgressBar
32 | *
33 | * @param max 进度条最大值
34 | * @param flowTime 每次变动进度时消耗的时间,时间越长进度条越平滑,并除以 10 作为 frequency
35 | */
36 | public SmoothProgressBar(int max, int flowTime) {
37 | super(0, max);
38 | this.flowTime = flowTime;
39 | frequency = flowTime / 10;
40 | }
41 |
42 | /**
43 | *
44 | * 以平滑方式设置进度
45 | *
46 | *
47 | * @param value 新进度
48 | */
49 | @Override
50 | public void setValue(int value) {
51 | singleThreadExecutor.execute(() -> {
52 | int currentValue = getValue();
53 | if (value < currentValue) {
54 | decrement(currentValue - value);
55 | } else {
56 | increment(value - currentValue);
57 | }
58 | });
59 | }
60 |
61 | @Override
62 | public void setVisible(boolean aFlag) {
63 | //保证在进度条在完成先前所有的 加/减 操作后,再进行 setVisible 操作
64 | singleThreadExecutor.execute(() -> super.setVisible(aFlag));
65 | }
66 |
67 | /**
68 | * 重置进度条进度至 0, 不使用平滑进度
69 | */
70 | public void reset() {
71 | super.setValue(0);
72 | }
73 |
74 | /**
75 | * 将进度条进度增长指定数值, 使用平滑方式
76 | *
77 | * @param value 增长的数值
78 | */
79 | public void increment(int value) {
80 | int currentValue = getValue();
81 | //如果变动的数值小于刷新速度,则使用变动数值作为刷新速度,否则使用默认刷新速度
82 | int finalFrequency = Math.min(frequency, value);
83 | for (int i = 1; i <= finalFrequency; i++) {
84 | int queueSize = singleThreadExecutor.getQueue().size();
85 |
86 | super.setValue(currentValue + ((value / finalFrequency) * i));
87 |
88 | //如果线程池中的任务过多则加快进度条速度(即降低 sleep 时间)
89 | if (queueSize >= 1) {
90 | ThreadUtil.safeSleep((flowTime / (finalFrequency + (i * TIME_MULTIPLIER))) * (1 / queueSize));
91 | } else {
92 | ThreadUtil.safeSleep(flowTime / (finalFrequency + (i * TIME_MULTIPLIER)));
93 | }
94 | }
95 |
96 | //如果最后进度条的值差异过大,则重新进行一次 increment
97 | int lastValue = (currentValue + value) - getValue();
98 | if (lastValue >= 3) {
99 | increment(lastValue);
100 | } else {
101 | //防止差异,设置为最终结果值
102 | super.setValue(currentValue + value);
103 | }
104 | }
105 |
106 | /**
107 | * 将进度条进度减少指定数值, 使用平滑方式
108 | *
109 | * @param value 减少的数值
110 | */
111 | public void decrement(int value) {
112 | int currentValue = getValue();
113 | //如果变动的数值小于刷新速度,则使用变动数值作为刷新速度,否则使用默认刷新速度
114 | int finalFrequency = Math.min(frequency, value);
115 | for (int i = 0; i < finalFrequency; i++) {
116 | int queueSize = singleThreadExecutor.getQueue().size();
117 |
118 | super.setValue(currentValue - ((value / finalFrequency) * i));
119 |
120 | //如果线程池中的任务过多则加快进度条速度(即降低 sleep 时间)
121 | if (queueSize >= 1) {
122 | ThreadUtil.safeSleep((flowTime / (finalFrequency + (i * TIME_MULTIPLIER))) * (1 / queueSize));
123 | } else {
124 | ThreadUtil.safeSleep(flowTime / (finalFrequency + (i * TIME_MULTIPLIER)));
125 | }
126 | }
127 |
128 | //如果最后进度条的值差异过大,则重新进行一次 increment
129 | int lastValue = (currentValue - value) - getValue();
130 | if (lastValue >= 3) {
131 | decrement(lastValue);
132 | } else {
133 | //防止差异,设置为最终结果值
134 | super.setValue(currentValue - value);
135 | }
136 | }
137 |
138 | @Override
139 | public void removeNotify() {
140 | super.removeNotify();
141 | singleThreadExecutor.shutdownNow();
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/SwingSystemTray.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui;
2 |
3 | import cn.hutool.core.io.IORuntimeException;
4 | import github.kasuminova.balloonserver.BalloonServer;
5 | import github.kasuminova.balloonserver.configurations.BalloonServerConfig;
6 | import github.kasuminova.balloonserver.configurations.ConfigurationManager;
7 |
8 | import javax.swing.*;
9 | import java.awt.*;
10 | import java.awt.event.MouseAdapter;
11 | import java.awt.event.MouseEvent;
12 | import java.awt.event.WindowAdapter;
13 | import java.awt.event.WindowEvent;
14 | import java.net.URL;
15 |
16 | import static github.kasuminova.balloonserver.BalloonServer.*;
17 | import static github.kasuminova.balloonserver.utils.SvgIcons.STOP_ICON;
18 | import static github.kasuminova.balloonserver.utils.SvgIcons.TERMINAL_ICON;
19 |
20 | /**
21 | * 中文系统托盘弹出菜单不乱码。
22 | * 网上抄的()
23 | *
24 | * @author Kasumi_Nova
25 | */
26 | public class SwingSystemTray {
27 | /**
28 | * 载入托盘
29 | *
30 | * @param frame 主窗口
31 | */
32 | public static void initSystemTrayAndFrame(JFrame frame) {
33 | //如果系统不支持任务栏,则设置为关闭窗口时退出程序
34 | if (!SystemTray.isSupported()) {
35 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
36 | return;
37 | }
38 | if (CONFIG.getCloseOperation() == BalloonServerConfig.EXIT_ON_CLOSE.getOperation()) {
39 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
40 | } else {
41 | frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
42 | }
43 | //使用JDialog 作为JPopupMenu载体
44 | JDialog dialog = new JDialog();
45 | //关闭JDialog的装饰器
46 | dialog.setUndecorated(true);
47 | //jDialog作为JPopupMenu载体不需要多大的size
48 | dialog.setSize(1, 1);
49 |
50 | //创建JPopupMenu
51 | //重写firePopupMenuWillBecomeInvisible
52 | //消失后将绑定的组件一起消失
53 | JPopupMenu trayMenu = new JPopupMenu() {
54 | @Override
55 | public void firePopupMenuWillBecomeInvisible() {
56 | dialog.setVisible(false);
57 | }
58 | };
59 | trayMenu.setSize(100, 30);
60 |
61 | //添加菜单选项
62 | JMenuItem exit = new JMenuItem("退出程序", STOP_ICON);
63 | exit.addActionListener(e -> {
64 | try {
65 | ConfigurationManager.saveConfigurationToFile(CONFIG, "./", "balloonserver");
66 | BalloonServer.GLOBAL_LOGGER.info("已保存主程序配置文件.");
67 | } catch (IORuntimeException ex) {
68 | BalloonServer.GLOBAL_LOGGER.error("保存主程序配置文件失败!");
69 | }
70 | //停止所有正在运行的服务器并保存配置
71 | stopAllServers(true);
72 | System.exit(0);
73 | });
74 | JMenuItem showMainFrame = new JMenuItem("显示窗口", TERMINAL_ICON);
75 | showMainFrame.addActionListener(e -> {
76 | //显示窗口
77 | frame.setVisible(true);
78 | frame.toFront();
79 | });
80 |
81 | trayMenu.add(showMainFrame);
82 | trayMenu.add(exit);
83 |
84 | URL resource = SwingSystemTray.class.getResource("/image/icon_16x16.png");
85 | // 创建托盘图标
86 | Image image = Toolkit.getDefaultToolkit().createImage(resource);
87 | // 创建系统托盘图标
88 | TrayIcon trayIcon = new TrayIcon(image);
89 | trayIcon.setToolTip("BalloonServer " + BalloonServer.VERSION);
90 | // 自动调整系统托盘图标大小
91 | trayIcon.setImageAutoSize(true);
92 |
93 | // 给托盘图标添加鼠标监听
94 | trayIcon.addMouseListener(new MouseAdapter() {
95 | @Override
96 | public void mouseReleased(MouseEvent e) {
97 | //左键点击
98 | if (e.getButton() == 1) {
99 | //显示窗口
100 | frame.setVisible(true);
101 | frame.toFront();
102 | } else if (e.getButton() == MouseEvent.BUTTON3 && e.isPopupTrigger()) {
103 | // 右键点击弹出JPopupMenu绑定的载体以及JPopupMenu
104 | dialog.setLocation(
105 | (int) (e.getXOnScreen() / SetupSwing.SCREEN_SCALE) + 5,
106 | (int) (e.getYOnScreen() / SetupSwing.SCREEN_SCALE) - trayMenu.getHeight() - 5);
107 | // 显示载体
108 | dialog.setVisible(true);
109 | dialog.toFront();
110 | // 在载体的 0,0 处显示对话框
111 | trayMenu.show(dialog, 0, 0);
112 | }
113 | }
114 | });
115 | // 添加托盘图标到系统托盘
116 | systemTrayAdd(trayIcon);
117 | // 关闭窗口时显示信息
118 | frame.addWindowListener(new WindowAdapter() {
119 | @Override
120 | public void windowClosed(WindowEvent e) {
121 | trayIcon.displayMessage("提示", "程序已最小化至后台运行。", TrayIcon.MessageType.INFO);
122 | }
123 | });
124 | }
125 |
126 | /**
127 | * 添加托盘图标到系统托盘中。
128 | *
129 | * @param trayIcon 系统托盘图标。
130 | */
131 | private static void systemTrayAdd(TrayIcon trayIcon) {
132 | // 将托盘图标添加到系统的托盘实例中
133 | SystemTray tray = SystemTray.getSystemTray();
134 | try {
135 | tray.add(trayIcon);
136 | } catch (AWTException ex) {
137 | GLOBAL_LOGGER.error(ex);
138 | }
139 | }
140 | }
141 |
142 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeCellRenderer.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.checkboxtree;
2 |
3 | import github.kasuminova.balloonserver.utils.SvgIcons;
4 |
5 | import javax.swing.*;
6 | import javax.swing.plaf.ColorUIResource;
7 | import javax.swing.tree.TreeCellRenderer;
8 | import javax.swing.tree.TreeNode;
9 | import java.awt.*;
10 |
11 |
12 | public class CheckBoxTreeCellRenderer extends JPanel implements TreeCellRenderer {
13 | protected final JCheckBox check;
14 | protected final CheckBoxTreeLabel label;
15 |
16 | public CheckBoxTreeCellRenderer() {
17 | setLayout(null);
18 | add(check = new JCheckBox());
19 | add(label = new CheckBoxTreeLabel());
20 | check.setBackground(UIManager.getColor("Tree.textBackground"));
21 | label.setForeground(UIManager.getColor("Tree.textForeground"));
22 | }
23 |
24 | /**
25 | * 返回的是一个{@code JPanel}对象,该对象中包含一个{@code JCheckBox}对象
26 | * 和一个{@code JLabel}对象。并且根据每个结点是否被选中来决定{@code JCheckBox}
27 | * 是否被选中。
28 | */
29 | @Override
30 | public Component getTreeCellRendererComponent(JTree tree, Object value,
31 | boolean selected, boolean expanded, boolean leaf, int row,
32 | boolean hasFocus) {
33 | String stringValue = tree.convertValueToText(value, selected, expanded, leaf, row, hasFocus);
34 | setEnabled(tree.isEnabled());
35 | check.setSelected(((CheckBoxTreeNode) value).isSelected());
36 | label.setFont(tree.getFont());
37 | label.setText(stringValue);
38 | label.setSelected(selected);
39 | label.setFocus(hasFocus);
40 | if (((TreeNode) value).getAllowsChildren()) {
41 | label.setIcon(SvgIcons.DIR_ICON);
42 | // label.setIcon(UIManager.getIcon("Tree.closedIcon"));
43 | // } else if (expanded) {
44 | // label.setIcon(UIManager.getIcon("Tree.openIcon"));
45 | } else {
46 | setFileIcon();
47 | }
48 |
49 | return this;
50 | }
51 |
52 | @Override
53 | public Dimension getPreferredSize() {
54 | Dimension dCheck = check.getPreferredSize();
55 | Dimension dLabel = label.getPreferredSize();
56 | return new Dimension(dCheck.width + dLabel.width, Math.max(dCheck.height, dLabel.height));
57 | }
58 |
59 | @Override
60 | public void doLayout() {
61 | Dimension dCheck = check.getPreferredSize();
62 | Dimension dLabel = label.getPreferredSize();
63 | int yCheck = 0;
64 | int yLabel = 0;
65 | if (dCheck.height < dLabel.height)
66 | yCheck = (dLabel.height - dCheck.height) / 2;
67 | else
68 | yLabel = (dCheck.height - dLabel.height) / 2;
69 | check.setLocation(0, yCheck);
70 | check.setBounds(0, yCheck, dCheck.width, dCheck.height);
71 | label.setLocation(dCheck.width, yLabel);
72 | label.setBounds(dCheck.width, yLabel, dLabel.width, dLabel.height);
73 | }
74 |
75 | @Override
76 | public void setBackground(Color color) {
77 | if (color instanceof ColorUIResource)
78 | color = null;
79 | super.setBackground(color);
80 | }
81 |
82 | public void setFileIcon() {
83 | String fileName = label.getText();
84 |
85 | if (fileName.endsWith(".class")) {
86 | label.setIcon(SvgIcons.CLASS_FILE_ICON);
87 | } else if (fileName.endsWith("doc") || fileName.endsWith("docx")) {
88 | label.setIcon(SvgIcons.DOC_FILE_ICON);
89 | } else if (fileName.endsWith("ppt") || fileName.endsWith("pptx")) {
90 | label.setIcon(SvgIcons.PPT_FILE_ICON);
91 | } else if (fileName.endsWith("xls") || fileName.endsWith("xlsx")) {
92 | label.setIcon(SvgIcons.XLS_FILE_ICON);
93 | } else if (fileName.endsWith("exe")) {
94 | label.setIcon(SvgIcons.EXE_FILE_ICON);
95 | } else if (fileName.endsWith("jar")) {
96 | label.setIcon(SvgIcons.JAR_FILE_ICON);
97 | } else if (fileName.endsWith("java")) {
98 | label.setIcon(SvgIcons.JAVA_FILE_ICON);
99 | } else if (fileName.endsWith("jpg")) {
100 | label.setIcon(SvgIcons.JPG_FILE_ICON);
101 | } else if (fileName.endsWith("json")) {
102 | label.setIcon(SvgIcons.JSON_FILE_ICON);
103 | } else if (fileName.endsWith("md")) {
104 | label.setIcon(SvgIcons.MD_FILE_ICON);
105 | } else if (fileName.endsWith("txt")) {
106 | label.setIcon(SvgIcons.TXT_FILE_ICON);
107 | } else if (fileName.endsWith("xml")) {
108 | label.setIcon(SvgIcons.XML_FILE_ICON);
109 | } else if (fileName.endsWith("yml")) {
110 | label.setIcon(SvgIcons.YML_FILE_ICON);
111 | } else if (fileName.endsWith("zip")) {
112 | label.setIcon(SvgIcons.ZIP_FILE_ICON);
113 | } else {
114 | label.setIcon(SvgIcons.FILE_ICON);
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeLabel.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.checkboxtree;
2 |
3 | import javax.swing.*;
4 | import javax.swing.plaf.ColorUIResource;
5 | import java.awt.*;
6 |
7 | public class CheckBoxTreeLabel extends JLabel {
8 | private boolean isSelected;
9 | private boolean hasFocus;
10 |
11 | @Override
12 | public void setBackground(Color color) {
13 | if (color instanceof ColorUIResource)
14 | color = null;
15 | super.setBackground(color);
16 | }
17 |
18 | @Override
19 | public void paint(Graphics g) {
20 | String str;
21 | if ((str = getText()) != null) {
22 | if (!str.isEmpty()) {
23 | if (isSelected)
24 | g.setColor(UIManager.getColor("Tree.selectionBackground"));
25 | else
26 | g.setColor(UIManager.getColor("Tree.textBackground"));
27 | Dimension dim = getPreferredSize();
28 | int imageOffset = 0;
29 | Icon currentIcon = getIcon();
30 | if (currentIcon != null)
31 | imageOffset = currentIcon.getIconWidth() + Math.max(0, getIconTextGap() - 1);
32 | g.fillRect(imageOffset, 0, dim.width - 1 - imageOffset, dim.height);
33 | if (hasFocus) {
34 | g.setColor(UIManager.getColor("Tree.selectionBorderColor"));
35 | g.drawRect(imageOffset, 0, dim.width - 1 - imageOffset, dim.height - 1);
36 | }
37 | }
38 | }
39 | super.paint(g);
40 | }
41 |
42 | @Override
43 | public Dimension getPreferredSize() {
44 | Dimension retDimension = super.getPreferredSize();
45 | if (retDimension != null)
46 | retDimension = new Dimension(retDimension.width + 3, retDimension.height);
47 | return retDimension;
48 | }
49 |
50 | public void setSelected(boolean isSelected) {
51 | this.isSelected = isSelected;
52 | }
53 |
54 | public void setFocus(boolean hasFocus) {
55 | this.hasFocus = hasFocus;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeNode.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.checkboxtree;
2 |
3 | import javax.swing.tree.DefaultMutableTreeNode;
4 |
5 | public class CheckBoxTreeNode extends DefaultMutableTreeNode {
6 | protected boolean isSelected;
7 |
8 | public CheckBoxTreeNode() {
9 | this(null);
10 | }
11 |
12 | public CheckBoxTreeNode(Object userObject) {
13 | this(userObject, true, false);
14 | }
15 |
16 | public CheckBoxTreeNode(Object userObject, boolean allowsChildren, boolean isSelected) {
17 | super(userObject, allowsChildren);
18 | this.isSelected = isSelected;
19 | }
20 |
21 | public CheckBoxTreeNode getChildAt(int index) {
22 | if (children == null) {
23 | throw new ArrayIndexOutOfBoundsException("node has no children");
24 | }
25 | return (CheckBoxTreeNode) children.elementAt(index);
26 | }
27 |
28 | public boolean isSelected() {
29 | return isSelected;
30 | }
31 |
32 | public void setSelected(boolean _isSelected) {
33 | this.isSelected = _isSelected;
34 |
35 | if (_isSelected) {
36 | // 如果选中,则将其所有的子结点都选中
37 | if (children != null) {
38 | for (Object obj : children) {
39 | CheckBoxTreeNode node = (CheckBoxTreeNode) obj;
40 | if (!node.isSelected)
41 | node.setSelected(true);
42 | }
43 | }
44 | // 向上检查,如果父结点的所有子结点都被选中,那么将父结点也选中
45 | CheckBoxTreeNode pNode = (CheckBoxTreeNode) parent;
46 | // 开始检查pNode的所有子节点是否都被选中
47 | if (pNode != null) {
48 | int index = 0;
49 | for (; index < pNode.children.size(); ++index) {
50 | CheckBoxTreeNode pChildNode = (CheckBoxTreeNode) pNode.children.get(index);
51 | if (!pChildNode.isSelected)
52 | break;
53 | }
54 | /*
55 | * 表明pNode所有子结点都已经选中,则选中父结点,
56 | * 该方法是一个递归方法,因此在此不需要进行迭代,因为
57 | * 当选中父结点后,父结点本身会向上检查的。
58 | */
59 | if (index == pNode.children.size()) {
60 | if (!pNode.isSelected)
61 | pNode.setSelected(true);
62 | }
63 | }
64 | } else {
65 | /*
66 | * 如果是取消父结点导致子结点取消,那么此时所有的子结点都应该是选择上的;
67 | * 否则就是子结点取消导致父结点取消,然后父结点取消导致需要取消子结点,但
68 | * 是这时候是不需要取消子结点的。
69 | */
70 | if (children != null) {
71 | int index = 0;
72 | for (; index < children.size(); ++index) {
73 | CheckBoxTreeNode childNode = (CheckBoxTreeNode) children.get(index);
74 | if (!childNode.isSelected)
75 | break;
76 | }
77 | // 从上向下取消的时候
78 | if (index == children.size()) {
79 | for (Object child : children) {
80 | CheckBoxTreeNode node = (CheckBoxTreeNode) child;
81 | if (node.isSelected)
82 | node.setSelected(false);
83 | }
84 | }
85 | }
86 |
87 | // 向上取消,只要存在一个子节点不是选上的,那么父节点就不应该被选上。
88 | CheckBoxTreeNode pNode = (CheckBoxTreeNode) parent;
89 | if (pNode != null && pNode.isSelected)
90 | pNode.setSelected(false);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeNodeSelectionListener.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.checkboxtree;
2 |
3 | import javax.swing.*;
4 | import javax.swing.tree.DefaultTreeModel;
5 | import javax.swing.tree.TreePath;
6 | import java.awt.event.MouseAdapter;
7 | import java.awt.event.MouseEvent;
8 |
9 | public class CheckBoxTreeNodeSelectionListener extends MouseAdapter {
10 | @Override
11 | public void mouseClicked(MouseEvent event) {
12 | JTree tree = (JTree) event.getSource();
13 | int x = event.getX();
14 | int y = event.getY();
15 | int row = tree.getRowForLocation(x, y);
16 | TreePath path = tree.getPathForRow(row);
17 | if (path != null) {
18 | CheckBoxTreeNode node = (CheckBoxTreeNode) path.getLastPathComponent();
19 | if (node != null) {
20 | boolean isSelected = !node.isSelected();
21 | node.setSelected(isSelected);
22 | ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(node);
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/fileobjectbrowser/FileObjectBrowser.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.fileobjectbrowser;
2 |
3 | import github.kasuminova.balloonserver.BalloonServer;
4 | import github.kasuminova.balloonserver.gui.checkboxtree.CheckBoxTreeCellRenderer;
5 | import github.kasuminova.balloonserver.gui.checkboxtree.CheckBoxTreeNode;
6 | import github.kasuminova.balloonserver.gui.checkboxtree.CheckBoxTreeNodeSelectionListener;
7 | import github.kasuminova.balloonserver.gui.layoutmanager.VFlowLayout;
8 | import github.kasuminova.balloonserver.updatechecker.ApplicationVersion;
9 | import github.kasuminova.balloonserver.utils.fileobject.AbstractSimpleFileObject;
10 | import github.kasuminova.balloonserver.utils.fileobject.SimpleDirectoryObject;
11 | import github.kasuminova.balloonserver.utils.fileobject.SimpleFileObject;
12 |
13 | import javax.swing.*;
14 | import javax.swing.border.TitledBorder;
15 | import javax.swing.tree.DefaultTreeModel;
16 | import javax.swing.tree.TreePath;
17 | import java.awt.*;
18 | import java.util.ArrayList;
19 |
20 | /**
21 | * @author Kasumi_Nova
22 | */
23 | public class FileObjectBrowser extends JDialog {
24 | public static final ApplicationVersion VERSION = new ApplicationVersion("1.0.0-BETA");
25 | public static final String TITLE = "FileObjectBrowser " + VERSION;
26 | public static final int WINDOW_WIDTH = 750;
27 | public static final int WINDOW_HEIGHT = 600;
28 |
29 | public FileObjectBrowser(SimpleDirectoryObject directoryObject) {
30 | setTitle(TITLE);
31 | setIconImage(BalloonServer.ICON.getImage());
32 | setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
33 | setResizable(false);
34 | setLocationRelativeTo(null);
35 |
36 | Container contentPane = getContentPane();
37 | contentPane.setLayout(new VFlowLayout());
38 |
39 | JPanel treePanel = new JPanel(new VFlowLayout(VFlowLayout.TOP, VFlowLayout.MIDDLE, 5, 5, 5, 5, true, false));
40 | treePanel.setBorder(new TitledBorder("服务端文件列表"));
41 |
42 | JTree tree = new JTree();
43 | CheckBoxTreeNode rootNode = new CheckBoxTreeNode("服务端文件夹");
44 | DefaultTreeModel model = new DefaultTreeModel(rootNode);
45 |
46 | tree.addMouseListener(new CheckBoxTreeNodeSelectionListener());
47 | tree.setModel(model);
48 | tree.setCellRenderer(new CheckBoxTreeCellRenderer());
49 |
50 | for (CheckBoxTreeNode checkBoxTreeNode : scanDirAndBuildTree(directoryObject)) {
51 | rootNode.add(checkBoxTreeNode);
52 | }
53 |
54 | tree.expandPath(new TreePath(rootNode.getPath()));
55 |
56 | JScrollPane treeScroll = new JScrollPane(tree);
57 | treePanel.add(treeScroll);
58 |
59 | contentPane.add(treePanel);
60 |
61 | setLocationRelativeTo(null);
62 | }
63 |
64 | private static ArrayList scanDirAndBuildTree(SimpleDirectoryObject directoryObject) {
65 | ArrayList treeNodes = new ArrayList<>(0);
66 |
67 | ArrayList fileObjects = directoryObject.getChildren();
68 | fileObjects.forEach((obj -> {
69 | if (obj instanceof SimpleFileObject fileObject) {
70 | CheckBoxTreeNode file = new CheckBoxTreeNode(fileObject.getName());
71 | file.setAllowsChildren(false);
72 |
73 | treeNodes.add(file);
74 | } else if (obj instanceof SimpleDirectoryObject dirObject){
75 | CheckBoxTreeNode dir = new CheckBoxTreeNode(dirObject.getName());
76 | dir.setAllowsChildren(true);
77 | scanDirAndBuildTree(dirObject).forEach(dir::add);
78 |
79 | treeNodes.add(dir);
80 | }
81 | }));
82 |
83 | return treeNodes;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/fileobjectbrowser/ImageListCellRenderer.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.fileobjectbrowser;
2 |
3 | import github.kasuminova.balloonserver.utils.ModernColors;
4 | import github.kasuminova.balloonserver.utils.SvgIcons;
5 | import github.kasuminova.balloonserver.utils.fileobject.AbstractSimpleFileObject;
6 | import github.kasuminova.balloonserver.utils.fileobject.SimpleDirectoryObject;
7 | import github.kasuminova.balloonserver.utils.fileobject.SimpleFileObject;
8 |
9 | import java.awt.*;
10 | import javax.swing.*;
11 |
12 | /**
13 | * @author Kasumi_Nova
14 | */
15 | public class ImageListCellRenderer extends DefaultListCellRenderer {
16 | public Component getListCellRendererComponent(JList> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
17 | if (value instanceof AbstractSimpleFileObject abstractFileObject) {
18 | setText(abstractFileObject.getName()); //设置文字
19 | if (abstractFileObject instanceof SimpleFileObject) {
20 | setIcon(SvgIcons.FILE_ICON);
21 | } else if (abstractFileObject instanceof SimpleDirectoryObject) {
22 | setIcon(SvgIcons.DIR_ICON);
23 | }
24 | } else {
25 | setText(value.toString()); //设置文字
26 | setIcon(SvgIcons.FILE_ICON);
27 | }
28 |
29 | if (isSelected) { //当某个元素被选中时
30 | setForeground(Color.WHITE); //设置前景色(文字颜色)为白色
31 | setBackground(ModernColors.BLUE); //设置背景色为蓝色
32 | } else { //某个元素未被选中时(取消选中)
33 | setForeground(list.getForeground()); //设置前景色(文字颜色)为黑色
34 | setBackground(list.getBackground()); //设置背景色为白色
35 | }
36 | return this;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/panels/AboutPanel.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.panels;
2 |
3 | import cn.hutool.core.img.ImgUtil;
4 | import github.kasuminova.balloonserver.BalloonServer;
5 | import github.kasuminova.balloonserver.gui.layoutmanager.VFlowLayout;
6 | import github.kasuminova.balloonserver.utils.MiscUtils;
7 |
8 | import javax.swing.*;
9 | import javax.swing.border.EmptyBorder;
10 | import java.awt.*;
11 |
12 | /**
13 | * @author Kasumi_Nova
14 | */
15 | public class AboutPanel {
16 | private static final Dimension ABOUT_BUTTON_SIZE = new Dimension(170, 30);
17 | private static final float TITLE_FONT_SIZE = 36F;
18 | private static final float LICENSE_LABEL_FONT_SIZE = 18F;
19 |
20 | public static JPanel createPanel() {
21 | //主面板
22 | JPanel aboutPanel = new JPanel(new BorderLayout());
23 | Box descBox = Box.createVerticalBox();
24 | //标题容器
25 | Box titleBox = Box.createHorizontalBox();
26 | titleBox.setBorder(new EmptyBorder(10,0,0,0));
27 | //LOGO, 并缩放图标
28 | titleBox.add(new JLabel(new ImageIcon(ImgUtil.scale(BalloonServer.ICON.getImage(), 80F / BalloonServer.ICON.getIconWidth()))));
29 | //标题
30 | JLabel title = new JLabel("BalloonServer " + BalloonServer.VERSION);
31 | title.setBorder(new EmptyBorder(0,10,0,0));
32 | //设置字体
33 | title.setFont(title.getFont().deriveFont(TITLE_FONT_SIZE));
34 | titleBox.add(title);
35 | //描述
36 | JPanel descPanel = new JPanel(new VFlowLayout(0, VFlowLayout.MIDDLE, 5, 5, 5, 5, false, false));
37 | descPanel.setBorder(new EmptyBorder(10,0,0,0));
38 | descPanel.add(new JLabel("BalloonServer 是 LittleServer 的衍生图形化版本, 基于 Netty-IO 的增强实现.", JLabel.CENTER));
39 | descPanel.add(new JLabel("提示: BalloonServer 内嵌了可视化更新规则编辑器, 你可以通过右键更新模式列表打开.", JLabel.CENTER));
40 | descPanel.add(new JLabel("提示: BalloonServer 支持启动多个服务端, 你可以使用窗口左上角菜单来管理多个实例.", JLabel.CENTER));
41 | //链接
42 | JPanel linkPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10,5));
43 | //仓库链接
44 | JButton openProjectLink = new JButton("点击打开仓库链接");
45 | openProjectLink.addActionListener(e -> MiscUtils.openLinkInBrowser("https://github.com/BalloonUpdate/BalloonServer"));
46 | openProjectLink.setPreferredSize(ABOUT_BUTTON_SIZE);
47 | linkPanel.add(openProjectLink);
48 | //项目链接
49 | JButton openOrganizationLink = new JButton("点击打开项目链接");
50 | openOrganizationLink.addActionListener(e -> MiscUtils.openLinkInBrowser("https://github.com/BalloonUpdate"));
51 | openOrganizationLink.setPreferredSize(ABOUT_BUTTON_SIZE);
52 | linkPanel.add(openOrganizationLink);
53 | //Issues 链接
54 | JButton openIssuesLink = new JButton("戳我提交 Issue!");
55 | openIssuesLink.addActionListener(e -> MiscUtils.openLinkInBrowser("https://github.com/BalloonUpdate/BalloonServer/issues/new"));
56 | openIssuesLink.setPreferredSize(ABOUT_BUTTON_SIZE);
57 | linkPanel.add(openIssuesLink);
58 | descPanel.add(linkPanel);
59 |
60 | descPanel.add(new JLabel("BalloonServer 的诞生离不开这些贡献: ", JLabel.CENTER));
61 | descPanel.add(new JLabel("Netty 为 BalloonServer 提供了高性能的并发网络框架;", JLabel.CENTER));
62 | descPanel.add(new JLabel("Alibaba FastJson2 为 BalloonServer 提供了高性能的 JSON 解析功能;", JLabel.CENTER));
63 | descPanel.add(new JLabel("FlatLaf, FlatLaf-Extra 为 BalloonServer 提供了一套完美的用户界面体验;", JLabel.CENTER));
64 | descPanel.add(new JLabel("Hutools 为 BalloonServer 提供了一系列的实用工具;实现了实时文件监听器的功能;", JLabel.CENTER));
65 | descPanel.add(new JLabel("以及任何积极使用该软件和为此软件出谋划策的用户和开发者们~", JLabel.CENTER));
66 |
67 | //协议
68 | JLabel licenseLabel = new JLabel("本软件使用 AGPLv3 协议.", JLabel.RIGHT);
69 | licenseLabel.setFont(licenseLabel.getFont().deriveFont(LICENSE_LABEL_FONT_SIZE));
70 | licenseLabel.setBorder(new EmptyBorder(0,0,10,10));
71 |
72 | descBox.add(titleBox);
73 | descBox.add(descPanel);
74 | aboutPanel.add(descBox);
75 | aboutPanel.add(licenseLabel, BorderLayout.SOUTH);
76 | return aboutPanel;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/gui/ruleeditor/RuleEditorActionListener.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.gui.ruleeditor;
2 |
3 | import cn.hutool.core.io.IORuntimeException;
4 | import cn.hutool.core.io.file.FileReader;
5 | import com.alibaba.fastjson2.JSONArray;
6 | import github.kasuminova.balloonserver.BalloonServer;
7 | import github.kasuminova.balloonserver.servers.GUIServerInterface;
8 | import github.kasuminova.balloonserver.servers.localserver.IntegratedServerInterface;
9 | import github.kasuminova.balloonserver.servers.remoteserver.RemoteClientInterface;
10 | import github.kasuminova.balloonserver.utils.GUILogger;
11 | import github.kasuminova.balloonserver.utils.HashCalculator;
12 | import github.kasuminova.balloonserver.utils.filecacheutils.JsonCacheUtils;
13 | import github.kasuminova.messages.RequestMessage;
14 |
15 | import javax.swing.*;
16 | import java.awt.event.ActionEvent;
17 | import java.awt.event.ActionListener;
18 | import java.io.File;
19 | import java.util.List;
20 |
21 | import static github.kasuminova.balloonserver.BalloonServer.*;
22 |
23 | public class RuleEditorActionListener implements ActionListener {
24 | protected final JList ruleList;
25 | protected final List rules;
26 | protected final GUIServerInterface serverInterface;
27 | protected final GUILogger logger;
28 |
29 | public RuleEditorActionListener(JList ruleList, List rules, GUIServerInterface serverInterface, GUILogger logger) {
30 | this.ruleList = ruleList;
31 | this.rules = rules;
32 | this.serverInterface = serverInterface;
33 | this.logger = logger;
34 | }
35 |
36 | @Override
37 | public void actionPerformed(ActionEvent e) {
38 | if (serverInterface.isGenerating().get()) {
39 | JOptionPane.showMessageDialog(MAIN_FRAME,
40 | "当前正在生成资源缓存,请稍后再试。",
41 | BalloonServer.TITLE,
42 | JOptionPane.WARNING_MESSAGE);
43 | return;
44 | }
45 |
46 | if (serverInterface instanceof IntegratedServerInterface) {
47 | File file = new File(String.format("./%s.%s.json", serverInterface.getServerName(), serverInterface.getResJsonFileExtensionName()));
48 | if (file.exists()) {
49 | int selection = JOptionPane.showConfirmDialog(MAIN_FRAME,
50 | "检测到本地 JSON 缓存,是否以 JSON 缓存启动规则编辑器?",
51 | BalloonServer.TITLE, JOptionPane.YES_NO_OPTION);
52 | if (!(selection == JOptionPane.YES_OPTION)) return;
53 |
54 | try {
55 | String json = new FileReader(file).readString();
56 | showRuleEditorDialog(JSONArray.parseArray(json), ruleList, rules, logger);
57 | } catch (IORuntimeException ex) {
58 | logger.error("无法读取本地 JSON 缓存\n", ex);
59 | }
60 | return;
61 | }
62 |
63 | int selection = JOptionPane.showConfirmDialog(MAIN_FRAME,
64 | "未检测到 JSON 缓存,是否立即生成 JSON 缓存并启动规则编辑器?",
65 | BalloonServer.TITLE, JOptionPane.YES_NO_OPTION);
66 | if (!(selection == JOptionPane.YES_OPTION)) return;
67 |
68 | GLOBAL_THREAD_POOL.execute(() -> {
69 | new JsonCacheUtils((IntegratedServerInterface) serverInterface, null, null).updateDirCache(null, HashCalculator.CRC32);
70 | if (file.exists()) {
71 | try {
72 | String json = new FileReader(file).readString();
73 | showRuleEditorDialog(JSONArray.parseArray(json), ruleList, rules, logger);
74 | } catch (IORuntimeException ex) {
75 | logger.error("无法读取本地 JSON 缓存\n", ex);
76 | }
77 | }
78 | });
79 | } else if (serverInterface instanceof RemoteClientInterface remoteClientInterface) {
80 | remoteClientInterface.getChannel().writeAndFlush(new RequestMessage(
81 | "GetJsonCache", List.of(new String[]{"RuleEditor"})));
82 | }
83 | }
84 |
85 | public static void showRuleEditorDialog(JSONArray jsonArray, JList ruleList, List rules, GUILogger logger) {
86 | GLOBAL_THREAD_POOL.execute(() -> {
87 | //锁定窗口,防止用户误操作
88 | MAIN_FRAME.setEnabled(false);
89 | RuleEditor editorDialog = new RuleEditor(jsonArray, rules, logger);
90 | editorDialog.setModal(true);
91 |
92 | MAIN_FRAME.setEnabled(true);
93 | editorDialog.setVisible(true);
94 |
95 | ruleList.setListData(rules.toArray(new String[0]));
96 | });
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/httpserver/ContentRanges.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.httpserver;
2 |
3 | import cn.hutool.core.util.StrUtil;
4 |
5 | import java.util.List;
6 |
7 | public class ContentRanges {
8 | private final long start;
9 | private final long end;
10 |
11 | public long getStart() {
12 | return start;
13 | }
14 |
15 | public long getEnd() {
16 | return end;
17 | }
18 |
19 | /**
20 | * 解析 range 字符串
21 | * @param rangeContent 要解析的字符串
22 | * @param type range 的类型:如 "bytes"
23 | * @param fileRange 文件长度
24 | */
25 | public ContentRanges(String rangeContent, String type, long fileRange) {
26 | if (rangeContent == null) {
27 | start = 0;
28 | end = fileRange;
29 | return;
30 | }
31 |
32 | String trueRangeContent = StrUtil.removePrefix(rangeContent, type + "=");
33 | if (trueRangeContent.startsWith("-")) {
34 | long last = Long.parseLong(trueRangeContent);
35 | start = fileRange + last;
36 | end = fileRange;
37 | return;
38 | }
39 |
40 | if ("0-0,-1".equals(trueRangeContent)) {
41 | start = 0;
42 | end = fileRange;
43 | return;
44 | }
45 |
46 | List ranges = StrUtil.split(trueRangeContent, '-', 2);
47 | start = Long.parseLong(ranges.get(0));
48 |
49 | end = ranges.get(1).isEmpty() ? fileRange : Long.parseLong(ranges.get(1));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/github/kasuminova/balloonserver/httpserver/DecodeProxy.java:
--------------------------------------------------------------------------------
1 | package github.kasuminova.balloonserver.httpserver;
2 |
3 | import github.kasuminova.balloonserver.utils.GUILogger;
4 | import io.netty.buffer.ByteBuf;
5 | import io.netty.channel.ChannelHandlerContext;
6 | import io.netty.handler.codec.ByteToMessageDecoder;
7 | import io.netty.util.Attribute;
8 | import io.netty.util.AttributeKey;
9 |
10 | import java.nio.charset.StandardCharsets;
11 | import java.util.List;
12 |
13 | /**
14 | * @Description nginx代理 netty tcp服务端负载均衡,nginx stream要打开 proxy_protocol on; 配置
15 | */
16 | public class DecodeProxy extends ByteToMessageDecoder {
17 | /**
18 | * 保存客户端IP
19 | */
20 | public static final AttributeKey key = AttributeKey.valueOf("IP");
21 | final GUILogger logger;
22 |
23 | public DecodeProxy(GUILogger logger) {
24 | this.logger = logger;
25 | }
26 |
27 | /**
28 | * decode() 会根据接收的数据,被调用多次,直到确定没有新的元素添加到list,
29 | * 或者是 ByteBuf 没有更多的可读字节为止。
30 | * 如果 list 不为空,就会将 list 的内容传递给下一个 handler
31 | *
32 | * @param ctx 上下文对象
33 | * @param byteBuf 入站后的 ByteBuf
34 | * @param out 将解码后的数据传递给下一个 handler
35 | */
36 | @Override
37 | protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List