Compare commits

...

72 Commits

Author SHA1 Message Date
a8db7cbf88 Release v1.0.3-beta
Release v1.0.3-beta
2024-11-17 17:06:02 +08:00
6a4e425f8a docs: version v1.0.3 2024-11-17 16:22:46 +08:00
09f515025c fix: 修复在AList运行时更新会报错“该文件正在被使用”的问题 2024-11-17 16:21:34 +08:00
96274f6952 chore: README.md 2024-11-11 22:53:27 +08:00
b468b9774d chore: 手动代理修改配置 2024-11-11 00:01:32 +08:00
17d21fdc03 chore: 日志输出处理 2024-11-05 22:32:21 +08:00
894439de6f docs: 打包配置 2024-10-30 09:43:33 +08:00
9bbc3f488f pref: 优化GUI更新功能;项目分为gui和upgrade两个模块 2024-10-30 00:18:20 +08:00
4dc13bb8f1 Release v1.0.2
Release v1.0.2
2024-10-27 15:29:57 +08:00
ea922e6a3f docs: version 1.0.2 2024-10-27 15:24:54 +08:00
3dc60a89e1 chore: 修改GUI配置文件位置 2024-10-19 22:03:38 +08:00
30f05240f9 fix: 补充设置界面语言切换 2024-10-19 17:33:05 +08:00
115e672f26 ci: 非release版本不上传工件 2024-10-19 17:14:41 +08:00
4988cdc31e feat: GUI单例模式 2024-09-26 22:18:55 +08:00
4e1af001c2 fix: 修复更新后版本号显示为配置文件内旧版本号的错误 2024-09-26 21:14:48 +08:00
ef7f4461c2 Release v1.0.1
fix Release v1.0.1
2024-09-26 20:47:52 +08:00
6670dc7210 ci: Modify the name of the file to be uploaded 2024-09-26 20:43:56 +08:00
98c4ec2cd8 Release v1.0.1
Release v1.0.1
2024-09-26 20:36:15 +08:00
7e0ec8b598 docs: version 1.0.1 2024-09-26 20:31:06 +08:00
47b001cdf8 Release v1.0.1
Release v1.0.1
2024-09-23 02:58:34 +08:00
b9e842a80c docs: README.md 添加界面截图
Some checks are pending
Auto Alpha Tag / auto-tag (x64, x64) (push) Waiting to run
Release / meta (push) Waiting to run
Release / windows (x64, x64) (push) Blocked by required conditions
Release / release (push) Blocked by required conditions
2024-09-21 20:11:48 +08:00
899f286682 style: 默认关闭托盘 2024-09-21 19:39:18 +08:00
229662bd15 pref: 开始下载后跳转主界面查看下载进度 2024-09-21 19:37:51 +08:00
6294fdbf42 chore: 修改类名 WindowsUtil -> ViewUtil 2024-09-21 19:36:21 +08:00
fbdf6b3ba7 feat: 添加GUI检查更新功能,添加nojre打包方式 2024-09-21 19:34:32 +08:00
828a2fdb62 fix: 修复代理类型下拉框无法跟随语言切换的问题
Some checks are pending
Auto Alpha Tag / auto-tag (x64, x64) (push) Waiting to run
Release / meta (push) Waiting to run
Release / windows (x64, x64) (push) Blocked by required conditions
Release / release (push) Blocked by required conditions
2024-09-21 12:46:21 +08:00
4702396bbc fix: 密码弹窗 2024-09-21 10:13:43 +08:00
0660674f7f pref: I18n 自动绑定
Some checks failed
Auto Alpha Tag / auto-tag (x64, x64) (push) Has been cancelled
Release / meta (push) Has been cancelled
Release / windows (x64, x64) (push) Has been cancelled
Release / release (push) Has been cancelled
2024-09-20 22:06:59 +08:00
1b3a4f5569 fix: 切换语言时theme重复bind导致报错的问题
Some checks failed
Auto Alpha Tag / auto-tag (x64, x64) (push) Has been cancelled
Release / meta (push) Has been cancelled
Release / windows (x64, x64) (push) Has been cancelled
Release / release (push) Has been cancelled
2024-09-19 12:34:52 +08:00
be7e17665f chore: clean up 2024-09-19 12:33:24 +08:00
654b13e150 fix: 托盘初始化时与主界面状态同步 2024-09-19 12:25:22 +08:00
329c484f4c ci: fix Auto Alpha Tag
Some checks are pending
Auto Alpha Tag / auto-tag (x64, x64) (push) Waiting to run
Release / meta (push) Waiting to run
Release / windows (x64, x64) (push) Blocked by required conditions
Release / release (push) Blocked by required conditions
2024-09-19 01:13:45 +08:00
ec585ea29d Release v1.0.0
Some checks are pending
Auto Alpha Tag / auto-tag (x64, x64) (push) Waiting to run
Release / meta (push) Waiting to run
Release / windows (x64, x64) (push) Blocked by required conditions
Release / release (push) Blocked by required conditions
Release v1.0.0
2024-09-18 20:48:50 +08:00
5a8e121fdd chore: change version 2024-09-18 20:40:15 +08:00
d4dcbf6e64 ci: add action 2024-09-18 20:39:37 +08:00
fb6c854dc0 docs: README.md, LICENSE 2024-09-18 03:27:01 +08:00
9c75f9bf3a chore: 调整 2024-09-17 02:40:32 +08:00
08f0473814 feat: 添加系统托盘、静默启动功能 2024-09-17 02:38:44 +08:00
442940cf05 pref: 开机自启改为执行cmd命令 2024-09-16 00:53:21 +08:00
12d9a07320 chore: 界面调整 2024-09-15 05:09:07 +08:00
9f5eaba2c8 feat: 添加开机自启 2024-09-15 04:35:43 +08:00
7c051bbf44 feat: 主界面调整、日志打印、代理测试、关于页、弹窗工具、清理代码 2024-09-15 04:24:36 +08:00
d65990791a fix: 修复网络代理设置端口时卡顿 2024-09-13 04:24:16 +08:00
c93837a7a9 feat: 主界面启动按钮、命令执行工具 2024-09-12 22:04:25 +08:00
cfd7075c8f feat: alist 检查更次 2024-09-12 22:02:22 +08:00
80bda287cc chore: 窗口图标 2024-09-12 04:38:10 +08:00
ef98a76cd3 feat: 主界面调整、日志打印 2024-09-12 01:39:28 +08:00
1f6ba2d8cc chore: 全局主题更换 2024-09-12 01:09:44 +08:00
a0e5e16afc feat: 主题颜色 2024-09-10 03:09:09 +08:00
a9dd63b251 feat: 代理测试
todo: 关于页
2024-09-09 20:15:49 +08:00
88a2f705ba chore: 弹窗工具 2024-09-09 20:12:20 +08:00
c3cbbd497f style: 清理代码 2024-09-08 21:53:27 +08:00
09ca87f4f4 style: 清理代码 2024-09-08 21:52:39 +08:00
95404edc92 style: 清理代码 2024-09-08 21:52:01 +08:00
d6609a5d75 style: pom.xml 2024-09-08 21:49:10 +08:00
7aaf2db034 fix: 打包插件配置 2024-09-08 16:11:51 +08:00
8588ad8c47 chore: 代理设置、删除依赖 2024-09-08 12:10:12 +08:00
f18ff10c92 fix: ikonli 打包错误 2024-09-08 12:03:20 +08:00
8d0a3d616c fix: pom.xml 2024-09-08 07:40:27 +08:00
956f7fd7ba pref: 获取系统网络代理设置 2024-09-08 07:34:36 +08:00
b1218e9122 todo: alist检查更新、代理测试 2024-09-08 02:03:01 +08:00
ef8ae3e53d pref: 获取平台网络代理配置 2024-09-08 01:39:19 +08:00
e00a742fff chore: .gitignore 2024-09-08 01:37:02 +08:00
4692e4b436 chore: 添加窗体阴影 2024-09-08 01:36:17 +08:00
fdbafdbb3f chore: AlertUtil 2024-09-08 01:32:18 +08:00
d1368daa61 chore: 修改拼写错误 2024-09-08 01:31:46 +08:00
edf574c0af pref: 修改Alert工具 2024-09-08 01:30:51 +08:00
6858dfcca8 chore: 删除生成的css、map文件 2024-09-08 01:27:45 +08:00
55ea1d8a80 chore: .gitignore 2024-09-05 19:12:52 +08:00
7b10186dab feat: 语言切换支持、布局重写、配置读取切换为jackson-yaml 2024-09-05 01:54:35 +08:00
fecdee3664 feat: 语言切换支持、布局重写、配置读取切换为jackson-yaml 2024-09-05 01:46:48 +08:00
7599524df1 add README.md 2024-08-28 06:24:41 +08:00
113 changed files with 7470 additions and 1591 deletions

79
.github/workflows/auto-alpha-tag.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Auto Alpha Tag
on:
push:
branches:
- "dev"
paths:
- ".github/workflows/*.yml"
- "src/**"
- "pom.xml"
- "!**/*.md"
pull_request:
branches:
- "dev"
types:
- closed
jobs:
auto-tag:
runs-on: windows-latest
permissions:
contents: write
actions: write
strategy:
matrix:
include:
- msbuild_target: x64
lowercase_target: x64
fail-fast: false
outputs:
tag: ${{ steps.set_tag.outputs.tag }}
pre_version: ${{ steps.set_tag.outputs.pre_version }}
main_tag_name: ${{ steps.push_main_tag.outputs.main_tag_name }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install semver
run: |
npm install --global --progress semver
- name: Set tag
id: set_tag
run: |
# pre_version是上一个公版这里需要拉上一个tag避免堆积过多commit
$latest_tag=$(git describe --tags --abbrev=0)
echo "latest_tag=$latest_tag" >> $env:GITHUB_OUTPUT
$described = $(git describe --tags --long --match 'v*')
$ids = $($described -split "-")
if ($ids.length -eq 3) {
$ver = "v$(semver --increment $ids[0].Substring(1))"
$pre_version = "$($ids[0])"
$dist = `printf "%03d"` $ids[1]
echo "tag=$ver-alpha.1.d$($dist).$($ids[2])" >> $env:GITHUB_OUTPUT
echo "pre_version=$pre_version" >> $env:GITHUB_OUTPUT
exit 0
}
if ($ids.length -eq 4) {
$dist = `printf "%03d"` $ids[2]
$pre_version = "$($ids[0])-$($ids[1])"
echo "pre_version=$pre_version" >> $env:GITHUB_OUTPUT
echo "tag=$pre_version.d$($dist).$($ids[3])" >> $env:GITHUB_OUTPUT
exit 0
}
exit 1
- name: Push tag to main repo
id: push_main_tag
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
$main_tag_name=$(echo "alpha/${{ steps.set_tag.outputs.tag }}")
git tag $main_tag_name -f
git push --tags origin HEAD:refs/tags/$main_tag_name -f
echo "main_tag_name=$main_tag_name" >> $env:GITHUB_OUTPUT

52
.github/workflows/auto-release-tag.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: Auto Tag Release PR
on:
pull_request:
branches:
- master
types:
- closed
workflow_dispatch:
inputs:
tag:
type: string
description: Release
required: true
jobs:
auto_tag_release:
if: github.event.pull_request.merged == true && (startsWith(github.event.pull_request.title, 'Release v') || startsWith(github.event.pull_request.title, 'release v')) || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.ALISTGUI_RELEASE }}
- name: Git config
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Extract tag name
id: extract_tag
run: |
if ${{ github.event_name != 'workflow_dispatch' }}; then
tag_name=$(echo "${{ github.event.pull_request.title }}" | sed -E 's/(Release|release)//' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
else
echo "tag_name=${{ inputs.tag }}" >> $GITHUB_OUTPUT
fi
- name: Create release tag and push
run: |
git tag -a "${{ steps.extract_tag.outputs.tag_name }}" -m "${{ steps.extract_tag.outputs.tag_name }}" -f
git push origin "${{ steps.extract_tag.outputs.tag_name }}"
- name: Merge into dev and push
run: |
git switch dev
git merge "${{ steps.extract_tag.outputs.tag_name }}"
git push origin dev

156
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,156 @@
name: Release
on:
push:
tags:
- "v*"
branches-ignore:
- "master"
paths:
- ".github/workflows/*.yml"
- "src/**"
- "pom.xml"
- "!**/*.md"
pull_request:
branches:
- "dev"
paths:
- ".github/workflows/*.yml"
- "src/**"
- "pom.xml"
- "!**/*.md"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
meta:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.set_tag.outputs.tag }}
prerelease: ${{ steps.set_pre.outputs.prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
path: temp
show-progress: false
- name: Fetch history
if: ${{ !startsWith(github.ref, 'refs/pull/') }}
run: |
git init
cp $GITHUB_WORKSPACE/temp/.git/config ./.git
rm -rf $GITHUB_WORKSPACE/temp
# git config remote.origin.fetch '+refs/*:refs/*'
git fetch --filter=tree:0 # --update-head-ok
git reset --hard origin/$(git branch --show-current) || true
git checkout ${{ github.ref_name }}
- name: Set tag
id: set_tag
run: |
${{ startsWith(github.ref, 'refs/pull/') && 'cd temp' || '' }}
echo tag=$(git describe --tags --match "v*" ${{ github.ref }} || git rev-parse --short HEAD) | tee -a $GITHUB_OUTPUT
exit ${PIPESTATUS[0]}
- name: Judge pre-release
id: set_pre
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
if [[ '${{ steps.set_tag.outputs.tag }}' =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo prerelease=false | tee -a $GITHUB_OUTPUT
else
echo prerelease=true | tee -a $GITHUB_OUTPUT
fi
# - name: Set up Node.js
# uses: actions/setup-node@v4
# with:
# node-version: 14
#
# - name: Install dependencies
# run: npm install conventional-changelog-cli
- name: Generate Changelog
run: |
this_tag=${{ steps.set_tag.outputs.tag }}
if [[ '${{ steps.set_pre.outputs.prerelease }}' != 'false' ]]; then
last_tag=$(git describe --tags --match "v*" --abbrev=0 --exclude='${{ steps.set_tag.outputs.tag }}')
else
last_tag=$(git describe --tags --match "v*" --abbrev=0 --exclude='${{ steps.set_tag.outputs.tag }}' --exclude='*-*')
fi
echo >> CHANGELOG.md
echo "**Full Changelog**: [$last_tag -> $this_tag](https://github.com/octopusYan/alist-gui/compare/${last_tag}...${this_tag})" >> CHANGELOG.md
- name: Upload changelog to Github
uses: actions/upload-artifact@v4
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
with:
name: changelog
path: CHANGELOG.md
windows:
needs: meta
strategy:
matrix:
include:
- msbuild_target: x64
lowercase_target: x64
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
show-progress: false
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'dragonwell'
architecture: x64
cache: maven
- name: Build with Maven
run: |
mvn clean package -f pom.xml
mkdir zipball && cp target/*.zip zipball
- name: Upload AListGUI to Github
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4
with:
name: AListGUI-windows
path: zipball
retention-days: 5
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [ meta, windows ]
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
steps:
- name: Download AListGUI from Github
uses: actions/download-artifact@v4
with:
path: assets
- name: Cleanup files
run: |
mv -vf assets/changelog/* .
cd assets
find . -type f | while read f; do mv -fvt . $f; done
- name: Release to Github
uses: softprops/action-gh-release@v2
with:
body_path: CHANGELOG.md
files: |
assets/*
prerelease: ${{ needs.meta.outputs.prerelease != 'false' }}

3
.gitignore vendored
View File

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

661
LICENSE Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

92
README.md Normal file
View File

@ -0,0 +1,92 @@
<div>
# AList GUI
![JDK](https://img.shields.io/badge/JDK-21-%2300599C)
[![JavaFX](https://img.shields.io/badge/JavaFX-21.0.4-%2300599C)](https://openjfx.io/)
![platform](https://img.shields.io/badge/platform-Windows-blueviolet)
<br>
[![license](https://img.shields.io/github/license/octopusYan/alist-gui)](https://github.com/octopusYan/alist-gui)
![commit](https://img.shields.io/github/commit-activity/m/octopusYan/alist-gui?color=%23ff69b4)
<br>
![stars](https://img.shields.io/github/stars/octopusYan/alist-gui?style=social)
![GitHub all releases](https://img.shields.io/github/downloads/octopusYan/alist-gui/total?style=social)
使用 JavaFx 编写的 (仿[AList Desktop](https://ad.nn.ci/zh) Windows GUI
</div>
### TODO
### 截图
<details open>
<summary> 主界面 </summary>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/909ac6ad-0021-47d7-a75c-7fb6505e8c15">
<img alt="main" src="https://github.com/user-attachments/assets/4984f7fb-acaa-4dbc-a322-8b6b89557cbf">
</picture>
</details>
<details open>
<summary> 管理员信息 </summary>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/840dca69-e67d-4083-88f8-8e67c3e47141">
<img alt="main" src="https://github.com/user-attachments/assets/a93d5967-65b5-4185-8bfb-77e55d811532">
</picture>
</details>
<details open>
<summary> 设置 </summary>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/8fc8c489-b9cd-4e34-ad32-4899ccc275e9">
<img alt="main" src="https://github.com/user-attachments/assets/f4cc78df-0718-4bac-9985-3761611f8f57">
</picture>
</details>
<details open>
<summary> 关于 </summary>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/dbef2d66-4ca4-4e89-8292-dbdce3566f93">
<img alt="about" src="https://github.com/user-attachments/assets/0e474a5d-78f3-4475-a8a9-fca15c3ed515">
</picture>
</details>
#### 本地运行
1. 克隆代码
```bash
git clone https://github.com/octopusYan/alist-gui
```
2. 运行
```bash
mvn clean javafx:run
```
#### 构建
1. 克隆代码
```bash
git clone https://github.com/octopusYan/alist-gui
```
2. 运行
```bash
mvn package
```
### 依赖/引用的项目
<figure>
| | |
|-----------------------------------------------------------------------------|-----------------|
| [JavaFX](https://openjfx.io/) | Java 桌面开发 |
| [AtlantaFX](https://mkpaz.github.io/atlantafx/) | JavaFX CSS 主题集合 |
| [JavaPackager](https://github.com/fvarrui/JavaPackager) | 打包插件 |
| [Ikonli](https://kordamp.org/ikonli/) | 图标库 |
| [Gluon-Emoji](https://github.com/gluonhq/emoji) | emoji |
| [Apache Commons](https://commons.apache.org/proper/commons-exec/index.html) | 工具包 |
| [Hutool](https://doc.hutool.cn/pages/index/) | 工具类库 |
| [SLF4J](https://slf4j.org/) | 日志工具 |
</figure>

298
gui/pom.xml Normal file
View File

@ -0,0 +1,298 @@
<?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>${gui.version}</version>
</parent>
<artifactId>gui</artifactId>
<version>${gui.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>
<parent.version>${gui.version}</parent.version>
<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>
<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>
<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>
<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>
<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>
<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

@ -5,7 +5,7 @@ package cn.octopusyan.alistgui;
*
* @author octopus_yan@foxmail.com
*/
public class AppLuncher {
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.debug(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,245 @@
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"));
}
util.destroy();
}
static ChangeListener<Boolean> restartListener = (_, _, run) -> {
if (run) return;
running.removeListener(AListManager.restartListener);
start();
};
public static void restart() {
if (!running.get()) {
start();
} else {
stop();
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 }====================================
/**
* 点击开始时检查 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;
}
/**
* 开始下载AList
*
* @param version 下载版本号
*/
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,265 @@
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.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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);
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 输出消息
* @return bbcode 颜色文本
*/
private static String resetConsoleColor(String msg) {
if (!msg.contains(CONSOLE_COLOR_PREFIX) || !Pattern.matches(CONSOLE_MSG_REX, msg)) return msg;
// 多颜色处理
String[] split = Pattern.compile("\\033\\[(\\d;)?(\\d+)m")
.matcher(msg)
.replaceAll(matchResult -> "\n" + matchResult.group())
.replaceFirst("\n", "")
.split("\n");
StringBuilder sb = new StringBuilder();
Pattern pattern = Pattern.compile("\\033\\[(\\d;)?(\\d+)m(.*)");
Matcher matcher;
for (int i = 0; i < split.length; i++) {
matcher = pattern.matcher(split[i]);
if(!matcher.matches()) continue;
if (i % 2 == 0) {
String color = StringUtils.lowerCase(Color.valueOf(Integer.parseInt(matcher.group(2))).getColor());
sb.append(color(color, matcher.group(3)));
} else {
sb.append(matcher.group(3));
}
}
return sb.toString();
}
/**
* @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

@ -1,5 +1,6 @@
package cn.octopusyan.alistgui.manager.http;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -14,7 +15,6 @@ import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.Executor;
@ -23,8 +23,15 @@ import java.util.concurrent.Executor;
*
* @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版本
*/
@ -84,19 +91,18 @@ public class HttpConfig {
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
// TODO Auto-generated method stub
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
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");//取消主机名验证
@ -104,80 +110,5 @@ public class HttpConfig {
} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.error("", e);
}
}
public HttpClient.Version getVersion() {
return version;
}
public void setVersion(HttpClient.Version version) {
this.version = version;
}
public int getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
public HttpClient.Redirect getRedirect() {
return redirect;
}
public void setRedirect(HttpClient.Redirect redirect) {
this.redirect = redirect;
}
public Executor getExecutor() {
return executor;
}
public void setExecutor(Executor executor) {
this.executor = executor;
}
public Authenticator getAuthenticator() {
return authenticator;
}
public void setAuthenticator(Authenticator authenticator) {
this.authenticator = authenticator;
}
public ProxySelector getProxySelector() {
return proxySelector;
}
public void setProxySelector(ProxySelector proxySelector) {
this.proxySelector = proxySelector;
}
public CookieHandler getCookieHandler() {
return cookieHandler;
}
public void setCookieHandler(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
}
public int getDefaultReadTimeout() {
return defaultReadTimeout;
}
public void setDefaultReadTimeout(int defaultReadTimeout) {
this.defaultReadTimeout = defaultReadTimeout;
}
public SSLContext getSslContext() {
return sslContext;
}
public SSLParameters getSslParameters() {
return sslParameters;
}
}

View File

@ -1,7 +1,14 @@
package cn.octopusyan.alistgui.manager.http;
import com.alibaba.fastjson2.JSONObject;
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;
@ -11,17 +18,22 @@ 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.List;
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;
@ -57,15 +69,20 @@ public class HttpUtil {
return builder.build();
}
public HttpUtil proxy(String host, int port) {
public void proxy(ProxySetup setup, ProxyInfo proxy) {
if (httpClient == null)
throw new RuntimeException("are you ready ?");
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(host, port);
ProxySelector other = ProxySelector.of(unresolved);
this.httpConfig.setProxySelector(other);
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);
return this;
}
public void clearProxy() {
@ -76,24 +93,20 @@ public class HttpUtil {
httpClient = createClient(httpConfig);
}
public HttpClient getHttpClient() {
return httpClient;
}
public String get(String uri, JSONObject header, JSONObject param) throws IOException, InterruptedException {
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, JSONObject header, JSONObject param) throws IOException, InterruptedException {
public String post(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri, header)
.POST(HttpRequest.BodyPublishers.ofString(param.toJSONString()));
.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, JSONObject header, JSONObject param) throws IOException, InterruptedException {
public String postForm(String uri, JsonNode header, JsonNode param) throws IOException, InterruptedException {
HttpRequest.Builder request = getRequest(uri + createFormParams(param), header)
.POST(HttpRequest.BodyPublishers.noBody());
@ -101,39 +114,65 @@ public class HttpUtil {
return response.body();
}
private HttpRequest.Builder getRequest(String uri, JSONObject header) {
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 (String key : header.keySet()) {
request.header(key, header.getString(key));
for (Map.Entry<String, JsonNode> property : header.properties()) {
String key = property.getKey();
request.header(key, JsonUtil.toJsonString(property.getValue()));
}
}
return request;
}
private String createFormParams(JSONObject params) {
private String createFormParams(JsonNode params) {
StringBuilder formParams = new StringBuilder();
if (params == null) {
return formParams.toString();
}
for (String key : params.keySet()) {
Object value = params.get(key);
if (value instanceof String) {
value = URLEncoder.encode(String.valueOf(value), StandardCharsets.UTF_8);
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 instanceof Number) {
formParams.append("&").append(key).append("=").append(value);
} else if (value instanceof List) {
formParams.append("&").append(key).append("=").append(params.getJSONArray(key));
} else if (value.isArray()) {
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
} else {
formParams.append("&").append(key).append("=").append(params.getJSONObject(key));
formParams.append("&").append(key).append("=").append(JsonUtil.toJsonString(value));
}
}
if (!formParams.isEmpty()) {
formParams = new StringBuilder("?" + formParams.substring(1));
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,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

@ -1,9 +1,11 @@
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 工具
@ -13,13 +15,17 @@ import java.nio.charset.StandardCharsets;
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),
null,
bundle,
new JavaFXBuilderFactory(),
null,
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,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,180 @@
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 javafx.beans.value.ChangeListener;
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 _ -> {
if(AListManager.isRunning()) {
AListManager.stop();
AListManager.runningProperty().addListener(updateListener);
} else {
// 下载完成后,解压并删除文件
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();
});
}
static final ChangeListener<Boolean> updateListener = (_, _, run) -> {
if (!run) {
// 下载完成后,解压并删除文件
DownloadUtil.unzip(ConfigManager.aList());
}
AListManager.runningProperty().removeListener(AboutViewModule.updateListener);
};
}

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,158 @@
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));
proxyHost.addListener((_, _, newValue) -> {
ConfigManager.proxyHost(newValue);
setProxy();
});
proxyPort.addListener((_, _, newValue) -> {
ConfigManager.proxyPort(newValue);
setProxy();
});
}
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 void setProxy() {
ConfigManager.checkProxy((success, _) -> {
if (!success) return;
HttpUtil.getInstance().proxy(ConfigManager.proxySetup(), ConfigManager.getProxyInfo());
});
}
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

@ -5,7 +5,8 @@
<property name="CHARSET" value="utf-8"/>
<property name="logback.app" value="alist-gui"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%highlight(%d{HH:mm:ss.SSS}) ${logback.app} %boldYellow([%thread]) %highlight(%-5level) %cyan(%logger{36}) - %mdc{client} [%X{trace_id}] %msg%n"/>
<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">

311
pom.xml
View File

@ -6,8 +6,9 @@
<groupId>cn.octopusyan</groupId>
<artifactId>alist-gui</artifactId>
<version>0.1.0</version>
<version>${gui.version}</version>
<name>alist-gui</name>
<packaging>pom</packaging>
<organization>
<name>octopus_yan</name>
@ -15,189 +16,177 @@
</organization>
<inceptionYear>2024</inceptionYear>
<description>alist windows gui</description>
<description>AList GUI</description>
<modules>
<module>upgrade</module>
<module>gui</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<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>
<gui.version>1.0.3</gui.version>
<junit.version>5.10.0</junit.version>
<javafx.version>17.0.6</javafx.version>
<javafx.version>21.0.4</javafx.version>
<slf4j.version>2.0.16</slf4j.version>
<logback.version>1.4.14</logback.version>
<fastjson.version>2.0.52</fastjson.version>
<hutool.version>5.8.25</hutool.version>
<hutool.version>5.8.32</hutool.version>
<common-lang3.version>3.16.0</common-lang3.version>
<common-io.version>2.16.1</common-io.version>
<common-exec.version>1.4.0</common-exec.version>
<jna.version>5.14.0</jna.version>
<lombok.version>1.18.32</lombok.version>
<jackson.version>2.15.4</jackson.version>
<ikonli.version>12.3.1</ikonli.version>
<gluonhq-emoji.version>1.0.1</gluonhq-emoji.version>
</properties>
<dependencies>
<!-- javafx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependencyManagement>
<!-- slf4j -->
<!-- https://slf4j.org/manual.html -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependencies>
<!-- junit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- javafx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- common -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${common-lang3.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${common-io.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>${common-exec.version}</version>
</dependency>
<!-- https://mkpaz.github.io/atlantafx/ -->
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
<version>2.0.1</version>
</dependency>
<!-- JSON -->
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- slf4j -->
<!-- https://slf4j.org/manual.html -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.kordamp.ikonli/ikonli-javafx -->
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>12.3.1</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-coreui-pack</artifactId>
<version>12.3.1</version>
</dependency>
<!-- common -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${common-lang3.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>${common-exec.version}</version>
</dependency>
</dependencies>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- https://kordamp.org/ikonli/ -->
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-fontawesome-pack</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>emoji</artifactId>
<version>${gluonhq-emoji.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<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>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>exe</nonFilteredFileExtension>
<nonFilteredFileExtension>dll</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</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>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<!-- Default configuration for running with: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<mainClass>cn.octopusyan.alistgui/cn.octopusyan.alistgui.AppLuncher
</mainClass>
<launcher>launcher</launcher>
<jlinkZipName>app</jlinkZipName>
<jlinkImageName>app</jlinkImageName>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
</plugin>
<plugin>
<groupId>io.github.fvarrui</groupId>
<artifactId>javapackager</artifactId>
<version>1.7.5</version>
<configuration>
<bundleJre>true</bundleJre>
<mainClass>cn.octopusyan.alistgui.AppLuncher</mainClass>
<generateInstaller>false</generateInstaller>
</configuration>
<executions>
<execution>
<id>bundling-for-windows</id>
<phase>package</phase>
<goals>
<goal>package</goal>
</goals>
<configuration>
<platform>windows</platform>
<createZipball>true</createZipball>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<!-- https://github.com/fvarrui/JavaPackager -->
<plugin>
<groupId>io.github.fvarrui</groupId>
<artifactId>javapackager</artifactId>
<version>1.7.7-SNAPSHOT</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@ -1,93 +0,0 @@
package cn.octopusyan.alistgui;
import cn.octopusyan.alistgui.config.AppConstant;
import cn.octopusyan.alistgui.config.CustomConfig;
import cn.octopusyan.alistgui.controller.MainController;
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.util.AlertUtil;
import cn.octopusyan.alistgui.util.FxmlUtil;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.util.Objects;
public class Application extends javafx.application.Application {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
@Override
public void init() throws Exception {
logger.info("application init ...");
// 初始化客户端配置
CustomConfig.init();
}
@Override
public void start(Stage primaryStage) throws IOException {
logger.info("application start ...");
// 初始化弹窗工具
AlertUtil.initOwner(primaryStage);
// http请求工具初始化
HttpConfig httpConfig = new HttpConfig();
if (CustomConfig.hasProxy()) {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(CustomConfig.proxyHost(), CustomConfig.proxyPort());
httpConfig.setProxySelector(ProxySelector.of(unresolved));
}
httpConfig.setConnectTimeout(10);
HttpUtil.init(httpConfig);
// 全局异常处理
Thread.setDefaultUncaughtExceptionHandler(this::showErrorDialog);
Thread.currentThread().setUncaughtExceptionHandler(this::showErrorDialog);
// 启动主界面
try {
FXMLLoader loader = FxmlUtil.load("root-view");
loader.setControllerFactory(c -> new MainController(primaryStage));
Parent root = loader.load();//底层面板
Scene scene = new Scene(root);
scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
scene.setFill(Color.TRANSPARENT);
primaryStage.setScene(scene);
primaryStage.initStyle(StageStyle.TRANSPARENT);
primaryStage.setTitle(String.format("%s v%s", AppConstant.APP_TITLE, AppConstant.APP_VERSION));
primaryStage.show();
MainController controller = loader.getController();
controller.setApplication(this);
} catch (Throwable t) {
showErrorDialog(Thread.currentThread(), t);
}
logger.info("application start over ...");
}
private void showErrorDialog(Thread t, Throwable e) {
logger.error("", e);
AlertUtil.exceptionAlert(new Exception(e)).show();
}
@Override
public void stop() throws Exception {
logger.info("application stop ...");
// 停止所有线程
ThreadPoolManager.getInstance().shutdown();
// 保存应用数据
CustomConfig.store();
}
}

View File

@ -1,203 +0,0 @@
package cn.octopusyan.alistgui.base;
import cn.octopusyan.alistgui.config.AppConstant;
import cn.octopusyan.alistgui.util.FxmlUtil;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import java.util.ResourceBundle;
/**
* 通用视图控制器基类
*
* @author octopus_yan@foxmail.com
*/
public abstract class BaseController<P extends Pane> implements Initializable {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private Application application;
private final Stage primaryStage;
private double xOffSet = 0, yOffSet = 0;
protected BaseController(Stage primaryStage) {
this.primaryStage = primaryStage;
}
public void jumpTo(BaseController<P> controller) throws IOException {
FXMLLoader fxmlLoader = FxmlUtil.load(controller.getRootFxml());
Scene scene = getRootPanel().getScene();
double oldHeight = getRootPanel().getPrefHeight();
double oldWidth = getRootPanel().getPrefWidth();
Pane root = fxmlLoader.load();
Stage stage = (Stage) scene.getWindow();
// 窗口大小
double newWidth = root.getPrefWidth();
double newHeight = root.getPrefHeight();
// 窗口位置
double newX = stage.getX() - (newWidth - oldWidth) / 2;
double newY = stage.getY() - (newHeight - oldHeight) / 2;
scene.setRoot(root);
stage.setX(newX < 0 ? 0 : newX);
stage.setY(newY < 0 ? 0 : newY);
stage.setWidth(newWidth);
stage.setHeight(newHeight);
controller = fxmlLoader.getController();
controller.setApplication(getApplication());
}
protected void open(Class<? extends BaseController<?>> clazz, String title) {
try {
FXMLLoader load = FxmlUtil.load(clazz.getDeclaredConstructor().newInstance().getRootFxml());
Parent root = load.load();
Scene scene = new Scene(root);
scene.getStylesheets().addAll(Objects.requireNonNull(getClass().getResource("/css/root.css")).toExternalForm());
Stage stage = new Stage();
stage.setScene(scene);
stage.setTitle(title);
stage.initOwner(getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.show();
load.getController();
} catch (Exception e) {
logger.error("", e);
}
}
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
// 全局窗口拖拽
if (dragWindow()) {
// 窗口拖拽
getRootPanel().setOnMousePressed(event -> {
xOffSet = event.getSceneX();
yOffSet = event.getSceneY();
});
getRootPanel().setOnMouseDragged(event -> {
Stage stage = (Stage) getWindow();
stage.setX(event.getScreenX() - xOffSet);
stage.setY(event.getScreenY() - yOffSet);
});
}
// 窗口初始化完成监听
getRootPanel().sceneProperty().addListener((observable, oldValue, newValue) -> {
newValue.windowProperty().addListener(new ChangeListener<Window>() {
@Override
public void changed(ObservableValue<? extends Window> observable, Window oldValue, Window newValue) {
//关闭窗口监听
getWindow().setOnCloseRequest(windowEvent -> onDestroy());
// app 版本信息
if (getAppVersionLabel() != null) getAppVersionLabel().setText("v" + AppConstant.APP_VERSION);
// 初始化数据
initData();
// 初始化视图样式
initViewStyle();
// 初始化视图事件
initViewAction();
}
});
});
}
public void setApplication(Application application) {
this.application = application;
}
public Application getApplication() {
return application;
}
public Stage getPrimaryStage() {
return primaryStage;
}
/**
* 窗口拖拽设置
*
* @return 是否启用
*/
public abstract boolean dragWindow();
/**
* 获取根布局
*
* @return 根布局对象
*/
public abstract P getRootPanel();
/**
* 获取根布局
* <p> 搭配 <code>FxmlUtil.load</code> 使用
*
* @return 根布局对象
* @see FxmlUtil#load(String)
*/
protected String getRootFxml() {
System.out.println(getClass().getSimpleName());
return "";
}
protected Window getWindow() {
return getRootPanel().getScene().getWindow();
}
/**
* App版本信息标签
*/
public Label getAppVersionLabel() {
return null;
}
/**
* 初始化数据
*/
public abstract void initData();
/**
* 视图样式
*/
public abstract void initViewStyle();
/**
* 视图事件
*/
public abstract void initViewAction();
/**
* 关闭窗口
*/
public void onDestroy() {
Stage stage = (Stage) getWindow();
stage.hide();
stage.close();
try {
Thread.sleep(1000);
Platform.exit();
} catch (InterruptedException e) {
logger.error("", e);
}
}
}

View File

@ -1,21 +0,0 @@
package cn.octopusyan.alistgui.config;
import cn.octopusyan.alistgui.util.PropertiesUtils;
import org.apache.commons.io.FileUtils;
import java.io.File;
/**
* 应用信息
*
* @author octopus_yan@foxmail.com
*/
public class AppConstant {
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 = System.getProperty("user.home") + File.separator + "AppData" + File.separator + "Local" + File.separator + APP_NAME;
public static final String TMP_DIR_PATH = FileUtils.getTempDirectoryPath() + APP_NAME;
public static final String CUSTOM_CONFIG_PATH = DATA_DIR_PATH + File.separator + "config.properties";
public static final String BAK_FILE_PATH = AppConstant.TMP_DIR_PATH + File.separator + "bak";
}

View File

@ -1,110 +0,0 @@
package cn.octopusyan.alistgui.config;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Properties;
/**
* 客户端设置
*
* @author octopus_yan@foxmail.com
*/
public class CustomConfig {
private static final Logger logger = LoggerFactory.getLogger(CustomConfig.class);
private static final Properties properties = new Properties();
public static final String PROXY_HOST_KEY = "proxy.host";
public static final String PROXY_PORT_KEY = "proxy.port";
public static void init() {
FileReader reader = null;
try {
File file = new File(AppConstant.CUSTOM_CONFIG_PATH);
if (!file.exists()) {
// 创建配置文件
if (!file.getParentFile().exists()) {
FileUtils.createParentDirectories(file);
}
boolean newFile = file.createNewFile();
// 保存配置
store();
} else {
reader = new FileReader(file);
properties.load(reader);
}
} catch (Exception e) {
logger.error("读取配置文件失败", e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
logger.error("关闭配置文件流", e);
}
}
}
/**
* 是否配置代理
*/
public static boolean hasProxy() {
String host = proxyHost();
Integer port = proxyPort();
return StringUtils.isNoneBlank(host) && Objects.nonNull(port);
}
/**
* 代理地址
*/
public static String proxyHost() {
return properties.getProperty(PROXY_HOST_KEY);
}
/**
* 代理地址
*/
public static void proxyHost(String host) {
properties.setProperty(PROXY_HOST_KEY, host);
}
/**
* 代理端口
*/
public static Integer proxyPort() {
try {
return Integer.parseInt(properties.getProperty(PROXY_PORT_KEY));
} catch (Exception ignored) {
}
return 10809;
}
/**
* 代理端口
*/
public static void proxyPort(int port) {
properties.setProperty(PROXY_PORT_KEY, String.valueOf(port));
}
/**
* 保存配置
*/
public static void store() {
// 生成配置文件
try {
properties.store(new PrintStream(AppConstant.CUSTOM_CONFIG_PATH), String.valueOf(StandardCharsets.UTF_8));
} catch (IOException e) {
logger.error("保存客户端配置失败", e);
}
}
}

View File

@ -1,102 +0,0 @@
package cn.octopusyan.alistgui.controller;
import cn.octopusyan.alistgui.base.BaseController;
import javafx.css.PseudoClass;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.TabPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* 主页面控制器
*
* @author octopus_yan@foxmail.com
*/
public class MainController extends BaseController<VBox> implements Initializable {
private double xOffset;
private double yOffset;
// 布局
@FXML
public VBox rootPane;
@FXML
public HBox windowHeader;
@FXML
public Button alwaysOnTopIcon;
@FXML
public Button minimizeIcon;
@FXML
public Button closeIcon;
// 界面
@FXML
public TabPane tabPane;
public MainController(Stage primaryStage) {
super(primaryStage);
}
/**
* 窗口拖拽设置
*
* @return 是否启用
*/
@Override
public boolean dragWindow() {
return false;
}
/**
* 获取根布局
*
* @return 根布局对象
*/
@Override
public VBox getRootPanel() {
return rootPane;
}
/**
* 初始化数据
*/
@Override
public void initData() {
}
/**
* 视图样式
*/
@Override
public void initViewStyle() {
}
/**
* 视图事件
*/
@Override
public void initViewAction() {
closeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> onDestroy());
minimizeIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> ((Stage) rootPane.getScene().getWindow()).setIconified(true));
alwaysOnTopIcon.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
boolean newVal = !getPrimaryStage().isAlwaysOnTop();
alwaysOnTopIcon.pseudoClassStateChanged(PseudoClass.getPseudoClass("always-on-top"), newVal);
getPrimaryStage().setAlwaysOnTop(newVal);
});
windowHeader.setOnMousePressed(event -> {
xOffset = getPrimaryStage().getX() - event.getScreenX();
yOffset = getPrimaryStage().getY() - event.getScreenY();
});
windowHeader.setOnMouseDragged(event -> {
getPrimaryStage().setX(event.getScreenX() + xOffset);
getPrimaryStage().setY(event.getScreenY() + yOffset);
});
}
}

View File

@ -1,228 +0,0 @@
package cn.octopusyan.alistgui.util;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 弹窗工具
*
* @author octopus_yan@foxmail.com
*/
public class AlertUtil {
private static Window mOwner;
private static Builder builder;
public static void initOwner(Stage stage) {
AlertUtil.mOwner = stage;
}
public static class Builder<T extends Dialog> {
T alert;
public Builder(T alert) {
this.alert = alert;
if (mOwner != null) this.alert.initOwner(mOwner);
}
public Builder<T> title(String title) {
alert.setTitle(title);
return this;
}
public Builder<T> header(String header) {
alert.setHeaderText(header);
return this;
}
public Builder<T> content(String content) {
alert.setContentText(content);
return this;
}
public Builder<T> icon(String path) {
icon(new Image(Objects.requireNonNull(this.getClass().getResource(path)).toString()));
return this;
}
public Builder<T> icon(Image image) {
getStage().getIcons().add(image);
return this;
}
public void show() {
if (AlertUtil.builder == null) {
AlertUtil.builder = this;
} else if (AlertUtil.builder.alert.isShowing()) {
if (!Objects.equals(AlertUtil.builder.alert.getContentText(), alert.getContentText()))
((Alert) AlertUtil.builder.alert).setOnHidden(event -> {
AlertUtil.builder = null;
show();
});
}
alert.showAndWait();
}
/**
* AlertUtil.confirm
*/
public void show(OnClickListener listener) {
Optional<ButtonType> result = alert.showAndWait();
listener.onClicked(result.get().getText());
}
/**
* AlertUtil.confirm
*/
public void show(OnChoseListener listener) {
Optional<ButtonType> result = alert.showAndWait();
if (result.get() == ButtonType.OK) {
listener.confirm();
} else {
listener.cancelOrClose(result.get());
}
}
/**
* AlertUtil.input
* 如果用户点击了取消按钮,将会返回null
*/
public String getInput() {
Optional<String> result = alert.showAndWait();
if (result.isPresent()) {
return result.get();
}
return null;
}
/**
* AlertUtil.choices
*/
public <R> R getChoice(R... choices) {
Optional result = alert.showAndWait();
return (R) result.get();
}
private Stage getStage() {
return (Stage) alert.getDialogPane().getScene().getWindow();
}
}
public static Builder<Alert> info(String content) {
return new Builder<Alert>(new Alert(Alert.AlertType.INFORMATION)).content(content).header(null);
}
public static Builder<Alert> info() {
return new Builder<Alert>(new Alert(Alert.AlertType.INFORMATION));
}
public static Builder<Alert> error(String message) {
return new Builder<Alert>(new Alert(Alert.AlertType.ERROR)).header(null).content(message);
}
public static Builder<Alert> warning() {
return new Builder<Alert>(new Alert(Alert.AlertType.WARNING));
}
public static Builder<Alert> exception(Exception ex) {
return new Builder<Alert>(exceptionAlert(ex));
}
public static Alert exceptionAlert(Exception ex) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Exception Dialog");
alert.setHeaderText(ex.getClass().getSimpleName());
alert.setContentText(ex.getMessage());
// 创建可扩展的异常。
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
String exceptionText = sw.toString();
Label label = new Label("The exception stacktrace was :");
TextArea 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);
GridPane expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
// 将可扩展异常设置到对话框窗格中。
alert.getDialogPane().setExpandableContent(expContent);
return alert;
}
/**
* 确认对话框
*/
public static Builder<Alert> confirm() {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("确认对话框");
return new Builder<Alert>(alert);
}
/**
* 自定义确认对话框 <p>
* <code>"Cancel"</code> OR <code>"取消"</code> 为取消按钮
*/
public static Builder<Alert> confirm(String... buttons) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
List<ButtonType> buttonList = Arrays.stream(buttons).map((type) -> {
ButtonBar.ButtonData buttonData = ButtonBar.ButtonData.OTHER;
if ("Cancel".equals(type) || "取消".equals(type))
buttonData = ButtonBar.ButtonData.CANCEL_CLOSE;
return new ButtonType(type, buttonData);
}).collect(Collectors.toList());
alert.getButtonTypes().setAll(buttonList);
return new Builder<Alert>(alert);
}
public static Builder<TextInputDialog> input(String content) {
TextInputDialog dialog = new TextInputDialog();
dialog.setContentText(content);
return new Builder<TextInputDialog>(dialog);
}
@SafeVarargs
public static <T> Builder<ChoiceDialog<T>> choices(String hintText, T... choices) {
ChoiceDialog<T> dialog = new ChoiceDialog<T>(choices[0], choices);
dialog.setContentText(hintText);
return new Builder<ChoiceDialog<T>>(dialog);
}
public interface OnChoseListener {
void confirm();
void cancelOrClose(ButtonType buttonType);
}
public interface OnClickListener {
void onClicked(String result);
}
}

View File

@ -1,28 +0,0 @@
package cn.octopusyan.alistgui.util;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
/**
* 剪切板工具
*
* @author octopus_yan@foxmail.com
*/
public class ClipUtil {
//获取系统剪切板
private static final Clipboard clipboard = Clipboard.getSystemClipboard();
public static void setClip(String data) {
clipboard.clear();
// 设置剪切板内容
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(data);
clipboard.setContent(clipboardContent);
}
public static String getString() {
// javafx 从剪切板获取文本
return clipboard.getString();
}
}

View File

@ -1,135 +0,0 @@
package cn.octopusyan.alistgui.util;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.CanReadFileFilter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 文件工具类
*
* @author octopus_yan@foxmail.com
*/
public class FileUtil {
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
public static File[] ls(String path) {
File dir = new File(path);
if (!dir.exists())
throw new RuntimeException(path + "不存在!");
if (!dir.isDirectory())
throw new RuntimeException(path + "不是一个文件夹!");
return dir.listFiles();
}
public static void copyFilesFromDir(String path, String dest) throws IOException {
if (StringUtils.isBlank(path) || StringUtils.isBlank(dest)) {
logger.error("path is blank !");
return;
}
File dir = new File(path);
if (!dir.exists()) {
logger.error("[" + path + "] 不存在!");
return;
}
if (!dir.isDirectory()) {
logger.error("[" + path + "] 不是一个文件夹!");
}
File[] files = dir.listFiles();
if (files == null) return;
File directory = new File(dest);
if (directory.exists() && !directory.isDirectory()) {
logger.error("[" + dest + "] 不是一个文件夹!");
}
FileUtils.forceMkdir(directory);
for (File file : files) {
copyFile(file, new File(dest + File.separator + file.getName()));
}
}
public static void copyFile(File in, File out) throws IOException {
copyFile(Files.newInputStream(in.toPath()), out);
}
public static void copyFile(InputStream input, File out) throws IOException {
OutputStream output = null;
try {
output = Files.newOutputStream(out.toPath());
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
} catch (IOException e) {
logger.error("", e);
} finally {
if (output != null) input.close();
if (output != null) output.close();
}
}
/**
* 获取文件主名称
*
* @param file 文件对象
* @return 文件名称
*/
public static String mainName(File file) {
//忽略判断
String fileName = file.getName();
return fileName.substring(0, fileName.lastIndexOf("."));
}
public static List<String> listFileNames(String path) {
Collection<File> files = FileUtils.listFiles(new File(path), CanReadFileFilter.CAN_READ, null);
return files.stream().map(File::getName).collect(Collectors.toList());
}
/**
* 返回被查找到的文件的绝对路径(匹配到一个就返回)
*
* @param root 根目录文件
* @param fileName 要找的文件名
* @return 绝对路径
*/
private static String findFiles(File root, String fileName) {
//定义一个返回值
String path = null;
//如果传进来的是目录,并且存在
if (root.exists() && root.isDirectory()) {
//遍历文件夹中的各个文件
File[] files = root.listFiles();
if (files != null) {
for (File file : files) {
//如果path的值没有变化
if (path == null) {
if (file.isFile() && file.getName().contains(fileName)) {
path = file.getAbsolutePath();
} else {
path = findFiles(file, fileName);
}
} else {
break;//跳出循环,增加性能
}
}
}
}
return path;
}
}

View File

@ -1,96 +0,0 @@
package cn.octopusyan.alistgui.util;
import org.apache.commons.exec.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* 命令工具类
*
* @author octopus_yan@foxmail.com
*/
public class ProcessesUtil {
private static final Logger logger = LoggerFactory.getLogger(ProcessesUtil.class);
private static final String NEWLINE = System.lineSeparator();
private static final DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
public static boolean exec(String command) {
try {
exec(command, new OnExecuteListener() {
@Override
public void onExecute(String msg) {
}
@Override
public void onExecuteSuccess(int exitValue) {
}
@Override
public void onExecuteError(Exception e) {
}
@Override
public void onExecuteOver() {
}
});
handler.waitFor();
} catch (Exception e) {
logger.error("", e);
}
return 0 == handler.getExitValue();
}
public static void exec(String command, OnExecuteListener listener) {
LogOutputStream logout = new LogOutputStream() {
@Override
protected void processLine(String line, int logLevel) {
if (listener != null) listener.onExecute(line + NEWLINE);
}
};
CommandLine commandLine = CommandLine.parse(command);
DefaultExecutor executor = DefaultExecutor.builder().get();
executor.setStreamHandler(new PumpStreamHandler(logout, logout));
DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler() {
@Override
public void onProcessComplete(int exitValue) {
if (listener != null) {
listener.onExecuteSuccess(exitValue);
}
}
@Override
public void onProcessFailed(ExecuteException e) {
if (listener != null) {
listener.onExecuteError(e);
}
}
};
try {
executor.execute(commandLine, handler);
} catch (IOException e) {
if (listener != null) listener.onExecuteError(e);
}
}
public interface OnExecuteListener {
void onExecute(String msg);
void onExecuteSuccess(int exitValue);
void onExecuteError(Exception e);
void onExecuteOver();
}
/**
* Prevent construction.
*/
private ProcessesUtil() {
}
}

View File

@ -1,17 +0,0 @@
module cn.octopusyan.alistgui {
requires java.net.http;
requires javafx.controls;
requires javafx.fxml;
requires javafx.graphics;
requires org.apache.commons.io;
requires org.apache.commons.lang3;
requires org.apache.commons.exec;
requires org.slf4j;
requires ch.qos.logback.core;
requires ch.qos.logback.classic;
requires com.alibaba.fastjson2;
exports cn.octopusyan.alistgui;
opens cn.octopusyan.alistgui to javafx.fxml;
opens cn.octopusyan.alistgui.controller to javafx.fxml;
}

View File

@ -1,3 +0,0 @@
app.name=${project.name}
app.title=alist gui
app.version=${project.version}

View File

@ -1,176 +0,0 @@
@import "root.css";
/**************************************************
* Window Header
**************************************************/
#windowHeader .iconButton {
/*-fx-max-height: 5px;*/
/*-fx-max-width: 5px;*/
-fx-border-radius: 15;
}
#rootPane #closeIcon {
-fx-color: #fa6057;
-fx-opacity: 0.5;
}
#rootPane #closeIcon:hover {
-fx-opacity: 1.0;
}
#rootPane #minimizeIcon {
-fx-color: #fbbc2e;
-fx-opacity: 0.5;
}
#rootPane #minimizeIcon:hover {
-fx-opacity: 1.0;
}
#rootPane #alwaysOnTopIcon {
-fx-color: #27c940;
-fx-opacity: 0.5;
}
#rootPane #alwaysOnTopIcon:hover {
-fx-opacity: 1.0;
}
#rootPane #alwaysOnTopIcon:always-on-top {
-fx-opacity: 1.0;
}
/**************************************************
* Tab label
**************************************************/
#tabPane .tab-header-area {
-fx-background-radius: 10;
-fx-background-color: #0000;
}
#tabPane .headers-region {
-fx-alignment: TOP_CENTER;
-fx-background-color: #18181a;
-fx-background-radius: 10;
-fx-padding: 5 0 5 0;
}
#tabPane .tab-header-background {
-fx-background-color: #0000;
}
#tabPane .tab {
-fx-text-fill: white;
-fx-padding: 10 20 10 20;
-fx-background-radius: 10;
-fx-background-color: #0000;
-fx-border-width: 0;
}
#tabPane .tab-label {
-fx-font-size: 15px;
-fx-text-fill: #707079;
}
#tabPane .tab:selected {
-fx-background-color: #2c69e0;
}
#tabPane .tab:selected .tab-label {
-fx-text-fill: white;
}
/**************************************************
* Main View
**************************************************/
#homeLabel {
-fx-font-size: 35px;
-fx-font-weight: bold;
-fx-text-fill: white;
-fx-font-family: 'JetBrains Mono';
}
#statusLabel {
-fx-padding: 2 5 2 5;
-fx-text-fill: black;
-fx-background-color: #1bc964;
-fx-background-radius: 10;
-fx-text-alignment: CENTER;
-fx-border-radius: 10;
-fx-border-color: black;
}
.controlMenu {
-fx-font-size: 15;
-fx-background-radius: 10px;
-fx-padding: 10 40 10 40;
-fx-border-radius: 10;
}
.controlMenu:focused {
-fx-opacity: 0.5;
}
#startButton {
-fx-background-color: #fa6057;
-fx-text-fill: white;
-fx-opacity: 1.0;
}
#passwordButton {
-fx-background-color: #1bc964;
-fx-opacity: 1.0;
}
#restartButton {
-fx-background-color: linear-gradient(#57b4f2, #9198e5);
-fx-text-fill: white;
-fx-opacity: 1.0;
}
#moreButton {
-fx-background-color: #0000;
-fx-text-fill: #9254d1;
-fx-border-color: #9254d1;
-fx-border-width: 2px;
-fx-opacity: 1.0;
}
#tabPane .tab:selected .focus-indicator {
-fx-border-width: 0;
-fx-border-color: #0000;
}
#logArea {
-fx-text-fill: #e3e4e4;
-fx-font-family: 'JetBrains Mono';
-fx-font-size: 20;
-fx-background-radius: 15;
-fx-border-radius: 15;
-fx-border-color: transparent;
-fx-background-insets: 0;
-fx-background-color: #18181c;
}
#logArea .content {
-fx-padding: 15;
-fx-background-radius: 15;
-fx-background-color: #18181c;
-fx-border-radius: 15;
-fx-border-color: transparent;
}
#logArea .scroll-pane {
-fx-background-color: transparent;
}
#logArea .scroll-pane .viewport{
-fx-background-color: transparent;
}
#logArea:focused {
-fx-background-radius: 15;
-fx-border-radius: 15;
-fx-border-color: transparent;
-fx-background-color: #18181c;
}

View File

@ -1,15 +0,0 @@
/**************************************************
* Root
**************************************************/
.rootPane {
-fx-background-color: black;
-fx-background-radius: 10;
-fx-border-radius: 10;
}
.root {
-fx-font-family: "Comic Sans MS";
}

View File

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.*?>
<VBox fx:id="rootPane" styleClass="rootPane" alignment="TOP_CENTER" prefHeight="620.0" prefWidth="700.0" spacing="10.0"
stylesheets="@../css/root-view.css" xmlns="http://javafx.com/javafx/11.0.14-internal"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="cn.octopusyan.alistgui.controller.MainController">
<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>
<Button fx:id="alwaysOnTopIcon" styleClass="iconButton"/>
<Button fx:id="minimizeIcon" styleClass="iconButton"/>
<Button fx:id="closeIcon" styleClass="iconButton"/>
</HBox>
<TabPane fx:id="tabPane" prefWidth="Infinity" VBox.vgrow="ALWAYS" tabClosingPolicy="UNAVAILABLE">
<padding>
<Insets bottom="10.0" left="20.0" right="20.0" top="10.0"/>
</padding>
<Tab text="主页">
<graphic>
<FontIcon iconLiteral="cil-library" iconColor="white"/>
</graphic>
<VBox>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<HBox styleClass="mainViewHeader" prefWidth="Infinity" alignment="TOP_CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<Label fx:id="homeLabel" text="AList GUI" alignment="CENTER"/>
<Label fx:id="statusLabel" text="运行中" alignment="TOP_CENTER">
<HBox.margin>
<Insets left="-10.0" top="-5"/>
</HBox.margin>
</Label>
</HBox>
<HBox prefWidth="Infinity" alignment="TOP_CENTER" 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" styleClass="controlMenu" text="开始"/>
<Button fx:id="passwordButton" styleClass="controlMenu" text="密码"/>
<Button fx:id="restartButton" styleClass="controlMenu" text="重启"/>
<Button fx:id="moreButton" styleClass="controlMenu" text="更多"/>
</HBox>
<TextArea fx:id="logArea" editable="false" wrapText="true" prefWidth="Infinity" VBox.vgrow="ALWAYS"
text="123d1a32s1d3as21d3a2s1d3a2s1d3a2s1d3a2s1d3a2s1d3a2s1d32aasda3s21da32s1d32a1sd">
<VBox.margin>
<Insets bottom="10.0" top="10.0"/>
</VBox.margin>
</TextArea>
<HBox prefWidth="Infinity" alignment="CENTER" spacing="25.0">
<padding>
<Insets bottom="10.0" top="30.0"/>
</padding>
<Button fx:id="docmentLabel" text="文档" textAlignment="CENTER">
<graphic>
<FontIcon iconLiteral="cib-readme"/>
</graphic>
</Button>
<Button fx:id="gethubLabel" text="Github" textAlignment="CENTER">
<graphic>
<FontIcon iconLiteral="cib-github"/>
</graphic>
</Button>
<Button fx:id="otherLabel" text="赞助" textAlignment="CENTER">
<graphic>
<FontIcon iconLiteral="cib-buy-me-a-coffee"/>
</graphic>
</Button>
</HBox>
</VBox>
</Tab>
<Tab fx:id="setupTab" text="设置">
<graphic>
<FontIcon iconLiteral="cil-settings" iconColor="white"/>
</graphic>
</Tab>
</TabPane>
</VBox>

Some files were not shown because too many files have changed in this diff Show More