Compare commits
566 Commits
1.0.7
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e9111db23 | ||
|
|
3ece32e427 | ||
|
|
dad4d26fd8 | ||
|
|
9b387c71fc | ||
|
|
7ac833b53b | ||
|
|
2327c5fd48 | ||
|
|
bfc63a3983 | ||
|
|
c727925791 | ||
|
|
cae1173180 | ||
|
|
10d2736232 | ||
|
|
97f01b7e3f | ||
|
|
21c7dd7a42 | ||
|
|
bbc64043ed | ||
|
|
79842f4625 | ||
|
|
626b344088 | ||
|
|
5b165ed587 | ||
|
|
d73b3b706e | ||
|
|
2928b35585 | ||
|
|
04bece21ff | ||
|
|
9e2e104baa | ||
|
|
0615378a17 | ||
|
|
013b03f9ef | ||
|
|
026b13ba05 | ||
|
|
6ec526eeeb | ||
|
|
e064bb9bb5 | ||
|
|
1f3fb5e2c0 | ||
|
|
5984f3e856 | ||
|
|
572c381e90 | ||
|
|
7a8ecb06bf | ||
|
|
4c928ac826 | ||
|
|
d07f9ede8c | ||
|
|
21a015bf8c | ||
|
|
71a1f5db4b | ||
|
|
96fd07a6ff | ||
|
|
733e062a7b | ||
|
|
e87a779adc | ||
|
|
9c6aa4dcb6 | ||
|
|
566b087eb1 | ||
|
|
2235e4c2a4 | ||
|
|
3b9d1f277b | ||
|
|
5110595404 | ||
|
|
034e0939be | ||
|
|
4ccfa82c8a | ||
|
|
d21ae5499a | ||
|
|
d80a9d48ab | ||
|
|
b305d6fd34 | ||
|
|
756fd305d1 | ||
|
|
f9549fbb7d | ||
|
|
e18b454fcc | ||
|
|
4f4ccfa7d4 | ||
|
|
5dfd5fefb2 | ||
|
|
a7ea4c70d2 | ||
|
|
b7796f58f0 | ||
|
|
c7bedc57e0 | ||
|
|
935f305ada | ||
|
|
8cf47a7ca1 | ||
|
|
c6c5ad711d | ||
|
|
5fc76d955a | ||
|
|
0aabe1b0dc | ||
|
|
820c4274e7 | ||
|
|
fcec30d70a | ||
|
|
f6dc0098f7 | ||
|
|
ca7b30bdb0 | ||
|
|
f73e7f4214 | ||
|
|
613a1ca78a | ||
|
|
bf9e3ea2e2 | ||
|
|
a4390c4c6d | ||
|
|
9cf317e245 | ||
|
|
d000d73122 | ||
|
|
88613ed2f6 | ||
|
|
2fc381caa5 | ||
|
|
30e245f7a3 | ||
|
|
35cf92e685 | ||
|
|
522ee44ca2 | ||
|
|
5cf03e1f1f | ||
|
|
afca4ddf0e | ||
|
|
ca757f975a | ||
|
|
79c304ae3d | ||
|
|
1848c869e7 | ||
|
|
029e570551 | ||
|
|
905c570e4c | ||
|
|
a3069229b8 | ||
|
|
1e930d61c9 | ||
|
|
0015c3a7fb | ||
|
|
4bfb87e5c7 | ||
|
|
4fbb626c42 | ||
|
|
35b175d944 | ||
|
|
5939297550 | ||
|
|
e6e5867742 | ||
|
|
bd9b73ad6a | ||
|
|
dbea769994 | ||
|
|
9cd83c4025 | ||
|
|
d4cc080e7b | ||
|
|
a324bc3d96 | ||
|
|
36929e9ea3 | ||
|
|
dd73b933d9 | ||
|
|
117a9ea692 | ||
|
|
2f932de295 | ||
|
|
679b24a74d | ||
|
|
c6b33ea828 | ||
|
|
a4ea8f2491 | ||
|
|
1c2315b5e9 | ||
|
|
d48e412580 | ||
|
|
3b3fb41384 | ||
|
|
190ac697fb | ||
|
|
8cdbf24cdc | ||
|
|
6e182b6813 | ||
|
|
3fa4064655 | ||
|
|
a77a03d8b3 | ||
|
|
5f8b9d36e2 | ||
|
|
1ed5e164de | ||
|
|
c67d5b0276 | ||
|
|
9646a98f6d | ||
|
|
aee34415a7 | ||
|
|
e4e70cc72c | ||
|
|
49779fe8f2 | ||
|
|
969ddc3662 | ||
|
|
de9b418c75 | ||
|
|
f8588745cd | ||
|
|
7c0cbab187 | ||
|
|
176fa64de0 | ||
|
|
495ab69195 | ||
|
|
93c28242fb | ||
|
|
57662f717b | ||
|
|
3669bd1f88 | ||
|
|
00e695b7d5 | ||
|
|
02c92e6019 | ||
|
|
8ba74f0846 | ||
|
|
79ed6d3858 | ||
|
|
8a66606275 | ||
|
|
3ebdf73fbf | ||
|
|
d249e5da5a | ||
|
|
7243e933e6 | ||
|
|
f92e43ee41 | ||
|
|
f6243e33da | ||
|
|
72f334d572 | ||
|
|
68cbb10dec | ||
|
|
45f5c4ee91 | ||
|
|
48c511613e | ||
|
|
c94063d459 | ||
|
|
c26aafd831 | ||
|
|
a5638329e7 | ||
|
|
8323f8eb5d | ||
|
|
35199ed094 | ||
|
|
b5d53cf416 | ||
|
|
39e26a6e3d | ||
|
|
15cb06af0f | ||
|
|
1e0bbb5a00 | ||
|
|
fb6fdbc14c | ||
|
|
96df53ce40 | ||
|
|
42f86dc3a3 | ||
|
|
32b11c6063 | ||
|
|
b2f43ba439 | ||
|
|
7d9cfc79cf | ||
|
|
5624a44104 | ||
|
|
2aaa6371ab | ||
|
|
fdc1b0c07d | ||
|
|
8ec7e416ea | ||
|
|
ae7730fb35 | ||
|
|
f99e3e9147 | ||
|
|
5fcda04544 | ||
|
|
aca0fbb1e3 | ||
|
|
4f8ffbf225 | ||
|
|
8b08edf38f | ||
|
|
e3d43111e8 | ||
|
|
6bb9a33a04 | ||
|
|
ca64880a01 | ||
|
|
c8d70e2a5a | ||
|
|
cee7eb8928 | ||
|
|
abc5f203d0 | ||
|
|
c3fc8f8e12 | ||
|
|
afd4f8baa9 | ||
|
|
b5ca8b988c | ||
|
|
89b22ba984 | ||
|
|
f329ef60df | ||
|
|
8acfdb8bca | ||
|
|
a7aec52f2a | ||
|
|
7f1317a9a7 | ||
|
|
a8a1fea91b | ||
|
|
675ad4608a | ||
|
|
72ba3757e2 | ||
|
|
c58e84d2ae | ||
|
|
18a7a5059b | ||
|
|
f0102b6f13 | ||
|
|
0cf8eb3c17 | ||
|
|
c08a9f2b18 | ||
|
|
728f1f2802 | ||
|
|
7310211fba | ||
|
|
1f3267de0a | ||
|
|
8ddad59c70 | ||
|
|
9ff6d0afa1 | ||
|
|
2341b09f81 | ||
|
|
5830aa937a | ||
|
|
56a9361e86 | ||
|
|
5868aa4d2f | ||
|
|
45135b7299 | ||
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 | ||
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d | ||
|
|
1c90fb4e18 | ||
|
|
c1f1d5185e | ||
|
|
aa4863712d | ||
|
|
247640f2e5 | ||
|
|
5b1f803fa8 | ||
|
|
accf590c17 | ||
|
|
19fbeab817 | ||
|
|
a785ab4680 | ||
|
|
5ee23cb379 | ||
|
|
145d2de001 | ||
|
|
8d3f5fe622 | ||
|
|
9ce4a88041 | ||
|
|
c0ecc9fa7d | ||
|
|
cb33a4468a | ||
|
|
168c4c5c64 | ||
|
|
9916edbd13 | ||
|
|
ab6b6a2127 | ||
|
|
c45f5f4c92 | ||
|
|
92ee2d72f2 | ||
|
|
a4364bcd6a | ||
|
|
d0827c3b0c | ||
|
|
036a04b0b3 | ||
|
|
eee016c643 | ||
|
|
472bf6e81f | ||
|
|
21229e352f | ||
|
|
1138f48a6e | ||
|
|
f044e0480e | ||
|
|
7047f17783 | ||
|
|
9308f15abb | ||
|
|
b2672f11fc | ||
|
|
f92c6586b2 | ||
|
|
69e07a9bd9 | ||
|
|
cdec60fd25 | ||
|
|
7c30933794 | ||
|
|
b892d2fe13 | ||
|
|
91ee463d41 | ||
|
|
169b66334c | ||
|
|
729eb99730 | ||
|
|
b1e62952f5 | ||
|
|
e21e9f9ed9 | ||
|
|
885c0a6337 | ||
|
|
09d837f5b8 | ||
|
|
b1e1f38b50 | ||
|
|
efa9613d26 | ||
|
|
287f6973f0 | ||
|
|
70fc5e3228 | ||
|
|
4bca15dbb0 | ||
|
|
ef2c57bb29 | ||
|
|
1135ecc5a3 | ||
|
|
e1b2e7b4db | ||
|
|
eec9154aeb | ||
|
|
fff2dd89c7 | ||
|
|
54116a4bf5 | ||
|
|
f28e785301 | ||
|
|
39b9bba9cf | ||
|
|
d7120cabe0 | ||
|
|
007318dae3 | ||
|
|
e25bd485ac | ||
|
|
7ba8e177b1 | ||
|
|
17082c5fb8 | ||
|
|
00dfb4ce39 | ||
|
|
cee0c863f8 | ||
|
|
a2a02c0bad | ||
|
|
891688d0ca | ||
|
|
3d47840aa8 | ||
|
|
01d0f9d4bd | ||
|
|
1c8abf9cba | ||
|
|
efd01da6f1 | ||
|
|
c929a794d5 | ||
|
|
85b2f222f4 | ||
|
|
899bd5a356 | ||
|
|
ded16873b0 | ||
|
|
501013ba31 | ||
|
|
76b1a2f4f8 | ||
|
|
e34fea0ecb | ||
|
|
7532546c77 | ||
|
|
187d5be658 | ||
|
|
2bf4d277be | ||
|
|
389c243ada | ||
|
|
bb39178b88 | ||
|
|
e1eab9db06 | ||
|
|
e6a45d25cd | ||
|
|
a64aef24b2 | ||
|
|
c9c8aa8f2a | ||
|
|
d3cfde5238 | ||
|
|
b4e82a4a0e | ||
|
|
40b4848cc8 | ||
|
|
0c4268a194 | ||
|
|
866c823c30 | ||
|
|
6f9cfc3b32 | ||
|
|
3717393ad9 | ||
|
|
10e7681ac2 | ||
|
|
51047e60f4 | ||
|
|
02984bb2a2 | ||
|
|
ea6b2d6a66 | ||
|
|
95bf08b0da | ||
|
|
15131fefd1 | ||
|
|
26a06b1c91 | ||
|
|
3cca89b917 | ||
|
|
d31510eb86 | ||
|
|
feaec3e223 | ||
|
|
97886e019f | ||
|
|
7cb6696bba | ||
|
|
ab017be855 | ||
|
|
6177bbdc68 | ||
|
|
ca484618c7 | ||
|
|
1f68f8a112 | ||
|
|
0cd5670bd3 | ||
|
|
8e9c6bcb68 | ||
|
|
6c1fa0fc53 | ||
|
|
5145cfa8a5 | ||
|
|
87b1a5e315 | ||
|
|
fa59869f2c | ||
|
|
1ae64fe0db | ||
|
|
f8d363836e | ||
|
|
38dccb1d22 | ||
|
|
3e31a89b92 | ||
|
|
d8f892cc02 | ||
|
|
873deb55aa | ||
|
|
c08712d79b | ||
|
|
61bc905727 | ||
|
|
17859be3c5 | ||
|
|
7a24e34695 | ||
|
|
58638eaad8 | ||
|
|
09d2f2d193 | ||
|
|
9121eff8d8 | ||
|
|
8b090b0526 | ||
|
|
15a0d642ff | ||
|
|
dc4333da21 | ||
|
|
184f6d46dc | ||
|
|
68788905fe | ||
|
|
fc46216a3f | ||
|
|
563143645e | ||
|
|
891ccb901b | ||
|
|
928a866fe7 | ||
|
|
ea25b5b46f | ||
|
|
1de10e6129 | ||
|
|
aaf9c2e8d2 | ||
|
|
b8196b5730 | ||
|
|
0a83e8beb4 | ||
|
|
bdf29b27e7 | ||
|
|
96da7eac41 | ||
|
|
71c0751692 | ||
|
|
442f334af2 | ||
|
|
48302a519f | ||
|
|
c00f759f15 | ||
|
|
1736dd909e | ||
|
|
1f01e368dd | ||
|
|
bfba958b7e | ||
|
|
758121b523 | ||
|
|
06e9a89e82 | ||
|
|
0ba6ac3305 | ||
|
|
993f220b8b | ||
|
|
8755c4ad23 | ||
|
|
77cb102dd6 | ||
|
|
89cfb0b451 | ||
|
|
6bdd83f208 | ||
|
|
8f86057dcc | ||
|
|
a7d7ffa2cc | ||
|
|
d51cbeee13 | ||
|
|
deb2a0151e | ||
|
|
e1c4e9312d | ||
|
|
c7233357bd | ||
|
|
eff8d565d0 | ||
|
|
932db49868 | ||
|
|
4d71c6cd05 | ||
|
|
96133e5abf | ||
|
|
f06e5d7dc1 | ||
|
|
d4b96edccf | ||
|
|
e9876d5b91 | ||
|
|
8b9a78a7bd | ||
|
|
6b48f577e9 | ||
|
|
da9b6c21d6 | ||
|
|
f1f889df14 | ||
|
|
ed65853ebe | ||
|
|
5ffdd219d9 | ||
|
|
4f84d6741c | ||
|
|
2568e7fcc8 | ||
|
|
dddbb49084 | ||
|
|
95846ab135 | ||
|
|
b5207e56c1 | ||
|
|
160771e912 | ||
|
|
0fbe180f3f | ||
|
|
41a0409e9e | ||
|
|
79e59143fb | ||
|
|
54e0f621ce | ||
|
|
4c8944d248 | ||
|
|
64bd95d8a8 | ||
|
|
1d88942e8e | ||
|
|
129e1b149a | ||
|
|
01aac98437 | ||
|
|
f9aaf7143f | ||
|
|
28174483f4 | ||
|
|
46412054c4 | ||
|
|
1ab0d26bab | ||
|
|
d90fb9aa35 | ||
|
|
744e64b359 | ||
|
|
2c5442f1f3 | ||
|
|
054c4701d2 | ||
|
|
54044625ea | ||
|
|
ca82704738 | ||
|
|
e98ec3fa8e | ||
|
|
6a4abf7e50 | ||
|
|
e2a6cceafd | ||
|
|
283404b6b9 | ||
|
|
c714f33a44 | ||
|
|
30fe047e5c | ||
|
|
827d814c7b | ||
|
|
ccb2c6daa0 | ||
|
|
1516d6d81e | ||
|
|
09b3655c4e | ||
|
|
614514c87e | ||
|
|
30cba6720d | ||
|
|
dce6551de2 | ||
|
|
95943cdeec | ||
|
|
18a26ee6bf | ||
|
|
f23aae371a | ||
|
|
757bc1c001 | ||
|
|
a19222dc60 | ||
|
|
24677ca4a6 | ||
|
|
0c5b6f8112 | ||
|
|
7c26e3d08a | ||
|
|
9b84fb4ec8 | ||
|
|
d8ec7b6d4a | ||
|
|
769c0d990b | ||
|
|
3f1ae38b61 | ||
|
|
e10fce21a2 | ||
|
|
a00557bb9d | ||
|
|
e478535ae5 | ||
|
|
7756758738 | ||
|
|
e0ea42faee | ||
|
|
e72c6b77b5 | ||
|
|
bcd3aacd6f | ||
|
|
570b0e08ad | ||
|
|
d703850e87 | ||
|
|
4bb1a411e8 | ||
|
|
9884ed19fa | ||
|
|
1ffaed3f36 | ||
|
|
4cb42953ad | ||
|
|
0248992dc3 | ||
|
|
9bab9db875 | ||
|
|
b283a3ea38 | ||
|
|
98ac2928b4 | ||
|
|
a0a6f43c10 | ||
|
|
0c158acbe0 | ||
|
|
9a97b3a304 | ||
|
|
aef44bd0da | ||
|
|
75c65d9ba8 | ||
|
|
93755db77f | ||
|
|
79d0a9a348 | ||
|
|
422e9aac84 | ||
|
|
9915c373b7 | ||
|
|
eba85e6348 | ||
|
|
483a7772f4 | ||
|
|
dcc96358f6 | ||
|
|
b5c30d505b | ||
|
|
1f3ef5f3f0 | ||
|
|
d388bcfc92 | ||
|
|
562c1f98fe | ||
|
|
f3c5009a45 | ||
|
|
09a1d9f51e | ||
|
|
84b48278ad | ||
|
|
ef9caf2578 | ||
|
|
b85bdf840e | ||
|
|
a2d7f3b5bb | ||
|
|
02a96d73c8 | ||
|
|
9fb12c7a71 | ||
|
|
145d8fc802 | ||
|
|
72c9dba806 | ||
|
|
de20bd654c | ||
|
|
35b3a10746 | ||
|
|
05fe6a0eb1 | ||
|
|
0552917c26 | ||
|
|
51c355c113 | ||
|
|
034ee3791d | ||
|
|
adabaf8f2d | ||
|
|
1f392c52a1 | ||
|
|
28fe4c725f | ||
|
|
18fe92cb11 | ||
|
|
c49acf7b51 | ||
|
|
7df317a1b9 | ||
|
|
219e5420f5 | ||
|
|
aefb7c3014 | ||
|
|
f0c7f06ff5 | ||
|
|
604e07b43a | ||
|
|
0000e4610a | ||
|
|
510324d7c4 | ||
|
|
33a359fcbf | ||
|
|
0b84d3271c | ||
|
|
57547c95cb | ||
|
|
503cfa9a4e | ||
|
|
af1f979e31 | ||
|
|
3cd9f92ea9 | ||
|
|
b332bada95 | ||
|
|
63a12c2ec8 | ||
|
|
743f242805 | ||
|
|
5bead0b27d | ||
|
|
73e3c7016b | ||
|
|
3829dcd0f9 | ||
|
|
b2047044fe | ||
|
|
47d1a13189 | ||
|
|
309909cbd7 | ||
|
|
b5cebb4cea | ||
|
|
b6dd2693cd | ||
|
|
5fdfe98f26 | ||
|
|
0c768aa1ca | ||
|
|
d493e6dc9e | ||
|
|
7e0c7d8891 | ||
|
|
3510c6600d | ||
|
|
32d91150bd | ||
|
|
bbf2d50e3f | ||
|
|
39725f9828 | ||
|
|
1e8c617a85 | ||
|
|
7f8573ec4c | ||
|
|
d8e629917e | ||
|
|
bdc0a15439 | ||
|
|
a25b97614f | ||
|
|
4e12c32566 | ||
|
|
ea9c0f1225 | ||
|
|
ff865f13a2 | ||
|
|
9875200912 | ||
|
|
9f218d004e | ||
|
|
ab727f66f4 | ||
|
|
efbc0302e4 | ||
|
|
ab2367d670 | ||
|
|
045e4f81d6 | ||
|
|
160cfee947 | ||
|
|
0e40b5ecce | ||
|
|
fcaddcee80 | ||
|
|
8d6295fd3b | ||
|
|
d0d51b3e6f | ||
|
|
b8d612f1d5 | ||
|
|
f7c49cde0c | ||
|
|
189f8fb3ba | ||
|
|
2a64bd28a8 | ||
|
|
8a733379a3 | ||
|
|
e5f854dfcd |
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gradle"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 25
|
||||||
33
.github/workflows/linux-x86-64.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: Linux x86-64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# download jdk
|
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-linux-x64-b825.69.tar.gz
|
|
||||||
|
|
||||||
# install jdk
|
|
||||||
- name: Installing Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'jdkfile'
|
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
|
||||||
java-version: '21.0.6'
|
|
||||||
architecture: x64
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-linux-x86-64
|
|
||||||
path: build/distributions/*.tar.gz
|
|
||||||
81
.github/workflows/linux.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: Linux
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
JBR_MAJOR: 21.0.8
|
||||||
|
JBR_PATCH: b1163.69
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-24.04-arm, ubuntu-latest ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradlexyz-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradlexyz-
|
||||||
|
|
||||||
|
- name: Set dynamic DOCKER_NAME
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_NAME=hstyi/jbr:${{ env.JBR_MAJOR }}${{ env.JBR_PATCH }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create docker-run.sh helper script
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cat <<'EOF' > docker-run.sh
|
||||||
|
#!/bin/bash
|
||||||
|
docker run --rm -v $HOME/.gradle:/root/.gradle -v "$(pwd)":/app -w /app "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x docker-run.sh
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :check-license && ./gradlew classes -x test'
|
||||||
|
|
||||||
|
- name: JLink
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh $DOCKER_NAME bash -c './gradlew :jar :copy-dependencies :plugins:migration:build :jlink'
|
||||||
|
|
||||||
|
- name: Package Deb
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh -e TERMORA_TYPE=deb $DOCKER_NAME bash -c './gradlew :jpackage && ./gradlew :dist'
|
||||||
|
|
||||||
|
- name: Package AppImage
|
||||||
|
shell: bash
|
||||||
|
run: ./docker-run.sh --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined $DOCKER_NAME bash -c 'rm -rf build/jpackage && ./gradlew :jpackage && ./gradlew :dist'
|
||||||
|
|
||||||
|
- name: Make ~/.gradle world-writable
|
||||||
|
shell: bash
|
||||||
|
run: sudo chmod -R 777 ~/.gradle
|
||||||
|
|
||||||
|
- name: Upload targz artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-targz-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.tar.gz
|
||||||
|
|
||||||
|
- name: Upload AppImage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-AppImage-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.AppImage
|
||||||
|
|
||||||
|
- name: Upload deb artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-linux-deb-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.deb
|
||||||
60
.github/workflows/osx-aarch64.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: macOS aarch64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-15
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
env:
|
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
|
||||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# create variables
|
|
||||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
|
||||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
||||||
|
|
||||||
# import certificate from secrets
|
|
||||||
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
|
||||||
|
|
||||||
# create temporary keychain
|
|
||||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
|
||||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# import certificate to keychain
|
|
||||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# download jdk
|
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-aarch64-b825.69.tar.gz
|
|
||||||
|
|
||||||
# install jdk
|
|
||||||
- name: Installing Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'jdkfile'
|
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
|
||||||
java-version: '21.0.6'
|
|
||||||
architecture: aarch64
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- name: Dist
|
|
||||||
env:
|
|
||||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
|
||||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
|
||||||
run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-osx-aarch64
|
|
||||||
path: build/distributions/*.dmg
|
|
||||||
61
.github/workflows/osx-x86-64.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: macOS x86-64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-13
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install the Apple certificate
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
env:
|
|
||||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
|
||||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# create variables
|
|
||||||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
|
||||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
||||||
|
|
||||||
# import certificate from secrets
|
|
||||||
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
|
||||||
|
|
||||||
# create temporary keychain
|
|
||||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
|
||||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# import certificate to keychain
|
|
||||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# download jdk
|
|
||||||
- run: wget -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-21.0.6-osx-x64-b825.69.tar.gz
|
|
||||||
|
|
||||||
# install jdk
|
|
||||||
- name: Installing Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'jdkfile'
|
|
||||||
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
|
||||||
java-version: '21.0.6'
|
|
||||||
architecture: x64
|
|
||||||
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- name: Dist
|
|
||||||
env:
|
|
||||||
TERMORA_MAC_SIGN: ${{ github.event_name == 'push' }}
|
|
||||||
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
|
||||||
run: |
|
|
||||||
./gradlew dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-osx-x86-64
|
|
||||||
path: build/distributions/*.dmg
|
|
||||||
105
.github/workflows/osx.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
name: macOS
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
TERMORA_MAC_SIGN: "${{ github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_SIGN_USER_NAME: ${{ secrets.TERMORA_MAC_SIGN_USER_NAME }}
|
||||||
|
# 只有发布版本时才需要公证
|
||||||
|
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||||
|
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
JBR_MAJOR: 21.0.8
|
||||||
|
JBR_PATCH: b1163.69
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-15-intel, macos-latest ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install the Apple certificate
|
||||||
|
if: ${{ fromJSON(env.TERMORA_MAC_SIGN) && env.BUILD_CERTIFICATE_BASE64 != '' }}
|
||||||
|
env:
|
||||||
|
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||||
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# create variables
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||||
|
|
||||||
|
# import certificate from secrets
|
||||||
|
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||||
|
|
||||||
|
# create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
# import certificate to keychain
|
||||||
|
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||||
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||||
|
|
||||||
|
- name: Setup the Notary information
|
||||||
|
if: ${{ fromJSON(env.TERMORA_MAC_NOTARY) && env.APPLE_ID != '' }}
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
STORE_CREDENTIALS: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||||
|
run: |
|
||||||
|
xcrun notarytool store-credentials "$STORE_CREDENTIALS" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APPLE_PASSWORD"
|
||||||
|
|
||||||
|
- name: Download Java
|
||||||
|
run: |
|
||||||
|
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
else
|
||||||
|
ARCH="x64"
|
||||||
|
fi
|
||||||
|
wget -q -O $RUNNER_TEMP/java_package.tar.gz https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-osx-$ARCH-${{ env.JBR_PATCH }}.tar.gz
|
||||||
|
|
||||||
|
# install jdk
|
||||||
|
- name: Installing Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'jdkfile'
|
||||||
|
jdkFile: ${{ runner.temp }}/java_package.tar.gz
|
||||||
|
java-version: '21.0.7'
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
- name: Install create-dmg
|
||||||
|
shell: bash
|
||||||
|
run: brew install create-dmg
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
shell: bash
|
||||||
|
run: ./gradlew :check-license && ./gradlew classes -x test
|
||||||
|
|
||||||
|
- name: JLink
|
||||||
|
shell: bash
|
||||||
|
run: ./gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
shell: bash
|
||||||
|
run: ./gradlew :jpackage && ./gradlew :dist
|
||||||
|
|
||||||
|
- name: Upload dmg artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-osx-dmg-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.dmg
|
||||||
29
.github/workflows/windows-x86-64.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: Windows x86-64
|
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Installing Java
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'jetbrains'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
# dist
|
|
||||||
- run: |
|
|
||||||
.\gradlew.bat dist --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: termora-windows-x86-64
|
|
||||||
path: |
|
|
||||||
build/distributions/*.zip
|
|
||||||
build/distributions/*.msi
|
|
||||||
116
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
name: Windows
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
JBR_MAJOR: 21.0.8
|
||||||
|
JBR_PATCH: b1163.69
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ windows-11-arm, windows-2022 ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup MSbuild
|
||||||
|
uses: microsoft/setup-msbuild@v2
|
||||||
|
|
||||||
|
- name: Set architecture
|
||||||
|
id: set-arch
|
||||||
|
run: |
|
||||||
|
if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
|
||||||
|
echo "ARCH=aarch64" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
echo "ARCH=x64" >> $env:GITHUB_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Find MakeAppx
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$installedRootsKey = "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots"
|
||||||
|
$kitsRoot = (Get-ItemProperty $installedRootsKey).KitsRoot10
|
||||||
|
$versions = Get-ChildItem -Path $installedRootsKey | Select-Object -ExpandProperty PSChildName
|
||||||
|
$maxVersion = $versions | ForEach-Object { [version]$_ } | Sort-Object -Descending | Select-Object -First 1
|
||||||
|
$arch = if ($env:ARCH -eq "aarch64") { "arm64" } else { "x64" }
|
||||||
|
$makeAppXPath = Join-Path -Path $kitsRoot -ChildPath "bin\$maxVersion\$arch\makeappx.exe"
|
||||||
|
Write-Output "MakeAppx.exe path: $makeAppXPath"
|
||||||
|
|
||||||
|
if (Test-Path $makeAppXPath) {
|
||||||
|
"MAKEAPPX_PATH=$makeAppXPath" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
|
} else {
|
||||||
|
Write-Output "MakeAppx.exe not found!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Install zip
|
||||||
|
run: |
|
||||||
|
$system32 = [System.Environment]::GetEnvironmentVariable("WINDIR") + "\System32"
|
||||||
|
Invoke-WebRequest -Uri "http://stahlworks.com/dev/zip.exe" -OutFile "$system32\zip.exe"
|
||||||
|
Invoke-WebRequest -Uri "http://stahlworks.com/dev/unzip.exe" -OutFile "$system32\unzip.exe"
|
||||||
|
|
||||||
|
- name: Install 7z
|
||||||
|
uses: milliewalky/setup-7-zip@v2
|
||||||
|
|
||||||
|
- name: Installing Java
|
||||||
|
run: |
|
||||||
|
$zipPath = "${{ runner.temp }}\java_package.zip"
|
||||||
|
$extractDir = "${{ runner.temp }}\jbr"
|
||||||
|
$url = "https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-${{ env.JBR_MAJOR }}-windows-${{ env.ARCH }}-${{ env.JBR_PATCH }}.zip"
|
||||||
|
curl -s --output $zipPath -L $url
|
||||||
|
unzip -q $zipPath -d $extractDir
|
||||||
|
$jbrDir = Get-ChildItem $extractDir | Select-Object -First 1
|
||||||
|
echo "JAVA_HOME=$($jbrDir.FullName)" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-gradle-
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
run: .\gradlew :check-license && .\gradlew classes -x test
|
||||||
|
|
||||||
|
- name: JLink
|
||||||
|
run: .\gradlew :jar :copy-dependencies :plugins:migration:build :jlink
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: .\gradlew :jpackage && .\gradlew :dist
|
||||||
|
|
||||||
|
- name: MSIX
|
||||||
|
env:
|
||||||
|
TERMORA_TYPE: appx
|
||||||
|
run: |
|
||||||
|
.\gradlew --stop
|
||||||
|
.\gradlew :dist
|
||||||
|
|
||||||
|
- name: Stop Gradle
|
||||||
|
run: .\gradlew.bat --stop
|
||||||
|
|
||||||
|
- name: Upload zip artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-windows-zip-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.zip
|
||||||
|
|
||||||
|
- name: Upload exe artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-windows-exe-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.exe
|
||||||
|
|
||||||
|
- name: Upload msix artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: termora-windows-msix-${{ runner.arch }}
|
||||||
|
path: |
|
||||||
|
build/distributions/*.msix
|
||||||
3
.github/workflows/winget.yml
vendored
@@ -7,7 +7,8 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: vedantmgoyal9/winget-releaser@main
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
|
if: github.repository == 'TermoraDev/termora'
|
||||||
with:
|
with:
|
||||||
identifier: TermoraDev.Termora
|
identifier: TermoraDev.Termora
|
||||||
installers-regex: 'x86-64\.msi$' # Only x86-64.msi files
|
installers-regex: '\.exe$'
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -6,6 +6,7 @@ certs/
|
|||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
.vs
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
124
README.md
@@ -1,51 +1,101 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
|
<a href="./README.zh_CN.md">简体中文</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme.png" alt="termora" />
|
<img src="docs/readme.png" alt="Readme" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
|
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- SSH and local terminal support
|
|
||||||
- Serial port protocol support
|
|
||||||
- [SFTP](./docs/sftp.png?raw=1) file transfer support
|
|
||||||
- Compatible with Windows, macOS, and Linux
|
|
||||||
- Zmodem protocol support
|
|
||||||
- SSH port forwarding & Jump hosts
|
|
||||||
- Terminal log
|
|
||||||
- Configuration synchronization via [Gist](https://gist.github.com)
|
|
||||||
- Macro support (record and replay scripts)
|
|
||||||
- Keyword highlighting
|
|
||||||
- Key management
|
|
||||||
- Broadcast commands to multiple sessions
|
|
||||||
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
|
||||||
- Data encryption
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
|
||||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
|
|
||||||
|
|
||||||
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
|
|
||||||
|
|
||||||
|
|
||||||
## LICENSE
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🧬 Cross-platform support
|
||||||
|
- 🔐 Built-in key manager
|
||||||
|
- 🖼️ X11 forwarding
|
||||||
|
- 🧑💻 SSH-Agent integration
|
||||||
|
- 💻 System information display
|
||||||
|
- 📁 GUI-based SFTP file management
|
||||||
|
- 📊 Nvidia GPU usage monitoring
|
||||||
|
- ⚡ Quick command shortcuts
|
||||||
|
|
||||||
|
|
||||||
|
## 🚀 File Transfer
|
||||||
|
|
||||||
|
- Direct transfers between server A ↔ B
|
||||||
|
- Recursive folder support
|
||||||
|
- Up to **6 concurrent transfer tasks**
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer.png" alt="Transfer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📝 File Editing
|
||||||
|
|
||||||
|
- Auto-upload after editing and saving
|
||||||
|
- Rename files and folders
|
||||||
|
- Quick deletion of large folders (`rm -rf` supported)
|
||||||
|
- Visual permission editing
|
||||||
|
- Create new files and folders
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 💻 Hosts
|
||||||
|
|
||||||
|
- Tree-like hierarchical structure, similar to folders
|
||||||
|
- Assign tags to individual hosts
|
||||||
|
- Import hosts from other tools
|
||||||
|
- Open with the transfer tool
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/host.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🧩 Plugins
|
||||||
|
|
||||||
|
- 🌍 Geo: Display geolocation of hosts
|
||||||
|
- 🔄 Sync: Sync settings to Gist or WebDAV
|
||||||
|
- 🗂️ WebDAV: Connect to WebDAV storage
|
||||||
|
- 📝 Editor: Built-in SFTP file editor
|
||||||
|
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
|
||||||
|
- ☁️ S3: Connect to S3 object storage
|
||||||
|
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
|
||||||
|
- ☁️ Tencent COS: Connect to Tencent Cloud COS
|
||||||
|
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
|
||||||
|
- 👉 [View all plugins...](https://www.termora.app/plugins)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📦 Download
|
||||||
|
|
||||||
|
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||||
|
- 🔨 **WinGet**: `winget install termora`
|
||||||
|
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Visit Termora in the Microsoft Store</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||||
|
|
||||||
|
- Run locally: `./gradlew :run`
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
This software is distributed under a dual-license model. You may choose one of the following options:
|
This software is distributed under a dual-license model. You may choose one of the following options:
|
||||||
|
|
||||||
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
- **AGPL-3.0**: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||||
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||||
|
|||||||
113
README.zh_CN.md
@@ -1,46 +1,99 @@
|
|||||||
# Termora
|
# Termora
|
||||||
|
|
||||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **Windows、macOS、Linux**。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
<img src="docs/readme-zh_CN.png" alt="Readme" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正在实现中) [**XTerm 控制序列协议**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)。未来目标是借助 [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html) 实现 **全平台支持**,包括 Android、iOS、iPadOS 等。
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- 支持 SSH 和本地终端
|
|
||||||
- 支持串口协议
|
|
||||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) 文件传输
|
|
||||||
- 支持 Windows、macOS、Linux 平台
|
|
||||||
- 支持 Zmodem 协议
|
|
||||||
- 支持 SSH 端口转发和跳板机
|
|
||||||
- 终端日志记录
|
|
||||||
- 支持配置同步到 [Gist](https://gist.github.com)
|
|
||||||
- 支持宏(录制脚本并回放)
|
|
||||||
- 支持关键词高亮
|
|
||||||
- 支持密钥管理器
|
|
||||||
- 支持将命令发送到多个会话
|
|
||||||
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
|
||||||
- 支持数据加密
|
|
||||||
- ...
|
|
||||||
|
|
||||||
## 下载
|
## ✨ 功能特性
|
||||||
|
|
||||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
- 🧬 跨平台运行
|
||||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
- 🔐 内建密钥管理器
|
||||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
- 🖼️ 支持 X11 转发
|
||||||
|
- 🧑💻 SSH-Agent 集成
|
||||||
|
- 💻 系统信息展示
|
||||||
|
- 📁 图形化 SFTP 文件管理
|
||||||
|
- 📊 Nvidia 显卡使用率查看
|
||||||
|
- ⚡ 快捷指令支持
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
## 🚀 文件传输
|
||||||
|
|
||||||
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
- 支持 A ↔ B 服务器间直接传输
|
||||||
|
- 文件夹递归复制支持
|
||||||
|
- 最多可同时运行 **6 个传输任务**
|
||||||
|
|
||||||
## 协议
|
<div align="center">
|
||||||
|
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
|
||||||
|
|
||||||
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
## 📝 文件编辑功能
|
||||||
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
|
||||||
|
- 保存后自动上传修改内容
|
||||||
|
- 文件 / 文件夹 重命名
|
||||||
|
- 快速删除大文件夹:`rm -rf` 支持
|
||||||
|
- 可视化更改权限
|
||||||
|
- 支持新建文件 / 文件夹
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 💻 主机
|
||||||
|
|
||||||
|
- 类似文件夹树形结构
|
||||||
|
- 给主机添加标签
|
||||||
|
- 从其它软件导入
|
||||||
|
- 使用传输工具打开
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 🧩 插件
|
||||||
|
|
||||||
|
- 🌍 Geo:显示主机位置信息
|
||||||
|
- 🔄 Sync:将配置同步至 Gist 或 WebDAV
|
||||||
|
- 🗂️ WebDAV:连接 WebDAV 对象存储
|
||||||
|
- 📝 Editor:内置 SFTP 文件编辑器
|
||||||
|
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
|
||||||
|
- ☁️ S3:连接 S3 对象存储
|
||||||
|
- ☁️ Huawei OBS:连接华为云对象存储
|
||||||
|
- ☁️ Tencent COS:连接腾讯云 COS
|
||||||
|
- ☁️ Alibaba OSS:连接阿里云 OSS
|
||||||
|
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 📦 下载
|
||||||
|
|
||||||
|
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||||
|
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||||
|
- 🪟 **WinGet**:`winget install termora`
|
||||||
|
- <img src="https://apps.microsoft.com/assets/icons/logo-16x16.png" alt="microsoft logo"/> <b>Microsoft Store</b>: <a href="https://apps.microsoft.com/store/detail/9NRZBHG43SB9?cid=DevShareMCLPCS">Termora</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🛠️ 开发指南
|
||||||
|
|
||||||
|
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||||
|
|
||||||
|
- 本地运行:`./gradlew :run`
|
||||||
|
|
||||||
|
|
||||||
|
## 📄 授权协议
|
||||||
|
|
||||||
|
Termora 采用双重许可方式,您可以选择:
|
||||||
|
|
||||||
|
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
||||||
|
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||||
|
|||||||
186
THIRDPARTY
@@ -1,240 +1,232 @@
|
|||||||
annotations 24.0.1
|
annotations
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
https://github.com/JetBrains/java-annotations/blob/master/LICENSE.txt
|
||||||
|
|
||||||
bip39-lib-jvm 1.0.8
|
colorpicker
|
||||||
MIT License
|
|
||||||
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
|
||||||
|
|
||||||
colorpicker 2.0.1
|
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
https://github.com/dheid/colorpicker/blob/main/LICENSE
|
||||||
|
|
||||||
commonmark 0.24.0
|
commonmark
|
||||||
BSD 2-Clause "Simplified" License
|
BSD 2-Clause "Simplified" License
|
||||||
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt
|
||||||
|
|
||||||
commons-codec 1.17.1
|
commons-codec
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
https://github.com/apache/commons-codec/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-compress 1.27.1
|
commons-vfs2
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
https://github.com/apache/commons-vfs/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-io 2.18.0
|
commons-io
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
https://github.com/apache/commons-io/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-lang3 3.17.0
|
commons-lang3
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
https://github.com/apache/commons-lang/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-net 3.11.1
|
commons-net
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
https://github.com/apache/commons-net/blob/master/LICENSE.txt
|
||||||
|
|
||||||
commons-text 1.12.0
|
commons-text
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
https://github.com/apache/commons-text/blob/master/LICENSE.txt
|
||||||
|
|
||||||
eddsa 0.3.0
|
commons-csv
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-csv/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
ini4j
|
||||||
|
Apache License 2.0
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
|
eddsa
|
||||||
Creative Commons Zero v1.0 Universal
|
Creative Commons Zero v1.0 Universal
|
||||||
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt
|
||||||
|
|
||||||
flatlaf 3.5.4
|
flatlaf
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf 3.5.4-no-natives
|
flatlaf-no-natives
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-extras 3.5.4
|
flatlaf-extras
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
flatlaf-swingx 3.5.4
|
flatlaf-swingx
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
|
||||||
|
|
||||||
JavaEWAH 1.2.3
|
JavaEWAH
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/lemire/javaewah/blob/master/LICENSE
|
https://github.com/lemire/javaewah/blob/master/LICENSE
|
||||||
|
|
||||||
jbr-api 17.1.10.1
|
jbr-api
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
https://github.com/JetBrains/JetBrainsRuntimeApi/blob/main/LICENSE
|
||||||
|
|
||||||
jcl-over-slf4j 1.7.36
|
jcl-over-slf4j
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0.txt
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
jfa 1.2.0
|
jfa
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
https://github.com/0x4a616e/jfa/blob/main/LICENSE
|
||||||
|
|
||||||
jgoodies-common 1.8.1
|
jgoodies-common
|
||||||
BSD-2-Clause License
|
BSD-2-Clause License
|
||||||
http://www.opensource.org/licenses/bsd-license.html
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
jgoodies-forms 1.9.0
|
jgoodies-forms
|
||||||
BSD-2-Clause License
|
BSD-2-Clause License
|
||||||
http://www.opensource.org/licenses/bsd-license.html
|
http://www.opensource.org/licenses/bsd-license.html
|
||||||
|
|
||||||
jna 5.16.0
|
jna
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||||
|
|
||||||
jna-platform 5.16.0
|
jna-platform
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/java-native-access/jna/blob/master/AL2.0
|
https://github.com/java-native-access/jna/blob/master/AL2.0
|
||||||
|
|
||||||
jnafilechooser-api 1.1.2
|
jnafilechooser-api
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
jnafilechooser-win32 1.1.2
|
jnafilechooser-win32
|
||||||
BSD 3-Clause "New" or "Revised" License
|
BSD 3-Clause "New" or "Revised" License
|
||||||
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
https://github.com/steos/jnafilechooser/blob/master/LICENSE
|
||||||
|
|
||||||
jsvg 1.4.0
|
jsvg
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
https://github.com/weisJ/jsvg/blob/master/LICENSE
|
||||||
|
|
||||||
jSystemThemeDetector 3.9.1
|
jSystemThemeDetector
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-logging 1.7.9
|
kotlin-logging
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
https://github.com/oshai/kotlin-logging/blob/master/LICENSE
|
||||||
|
|
||||||
kotlin-stdlib 2.1.0
|
kotlin-stdlib
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk7 1.9.10
|
kotlin-reflect
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk8 1.9.10
|
kotlin-stdlib-jdk7
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlin-stdlib-jdk8 1.9.10
|
kotlin-stdlib-jdk8
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
kotlinx-coroutines-core-jvm 1.10.1
|
kotlin-stdlib-jdk8
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
|
||||||
|
|
||||||
|
restart4j
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/hstyi/restart4j/blob/main/LICENSE
|
||||||
|
|
||||||
|
kotlinx-coroutines-core
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
kotlinx-coroutines-swing 1.10.1
|
kotlinx-coroutines-swing
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
kotlinx-serialization-core-jvm 1.7.3
|
kotlinx-serialization-json
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
||||||
|
|
||||||
kotlinx-serialization-json-jvm 1.7.3
|
logging-interceptor
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
logging-interceptor 4.12.0
|
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
okhttp 4.12.0
|
okhttp
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
okio-jvm 3.6.0
|
okio-jvm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
org.eclipse.jgit.ssh.apache 7.1.0.202411261347-r
|
org.eclipse.jgit.ssh.apache
|
||||||
Eclipse Distribution License
|
Eclipse Distribution License
|
||||||
https://www.eclipse.org/org/documents/edl-v10.php
|
https://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
|
||||||
org.eclipse.jgit 7.1.0.202411261347-r
|
org.eclipse.jgit.ssh.apache.agent
|
||||||
Eclipse Distribution License
|
Eclipse Distribution License
|
||||||
https://www.eclipse.org/org/documents/edl-v10.php
|
https://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
|
||||||
oshi-core 6.6.5
|
org.eclipse.jgit
|
||||||
|
Eclipse Distribution License
|
||||||
|
https://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
|
||||||
|
oshi-core
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/oshi/oshi/blob/master/LICENSE
|
https://github.com/oshi/oshi/blob/master/LICENSE
|
||||||
|
|
||||||
pty4j 0.13.2
|
pty4j
|
||||||
Eclipse Public License 1.0
|
Eclipse Public License 1.0
|
||||||
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
https://github.com/JetBrains/pty4j/blob/master/LICENSE
|
||||||
|
|
||||||
slf4j-api 2.0.16
|
slf4j-api
|
||||||
MIT License
|
MIT License
|
||||||
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
https://github.com/qos-ch/slf4j/blob/master/LICENSE.txt
|
||||||
|
|
||||||
slf4j-tinylog 2.7.0
|
slf4j-tinylog
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
sshd-common 2.14.0
|
sshd-common
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-core 2.14.0
|
sshd-core
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-osgi 2.14.0
|
sshd-osgi
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
sshd-sftp 2.14.0
|
sshd-sftp
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
swingx-all 1.6.5-1
|
swingx-all
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE v3
|
GNU LESSER GENERAL PUBLIC LICENSE v3
|
||||||
https://www.gnu.org/licenses/lgpl-3.0
|
https://www.gnu.org/licenses/lgpl-3.0
|
||||||
|
|
||||||
tinylog-api 2.7.0
|
tinylog-api
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
tinylog-impl 2.7.0
|
tinylog-impl
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
https://github.com/tinylog-org/tinylog/blob/v2.7/license.txt
|
||||||
|
|
||||||
versioncompare 1.4.1
|
versioncompare
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
https://github.com/G00fY2/version-compare/blob/main/LICENSE
|
||||||
|
|
||||||
xodus-compress 2.0.1
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-environment 2.0.1
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-openAPI 2.0.1
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-utils 2.0.1
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
xodus-vfs 2.0.1
|
|
||||||
Apache License 2.0
|
|
||||||
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
|
||||||
|
|
||||||
jediterm
|
jediterm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
https://github.com/JetBrains/jediterm/blob/master/LICENSE-APACHE-2.0.txt
|
||||||
|
|
||||||
mixpanel-java 1.5.3
|
mixpanel-java
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||||
|
|
||||||
@@ -242,6 +234,34 @@ json-20231013
|
|||||||
Public Domain.
|
Public Domain.
|
||||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||||
|
|
||||||
jSerialComm 2.11.0
|
jSerialComm
|
||||||
Apache License 2.0
|
Apache License 2.0
|
||||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||||
|
|
||||||
|
exposed-core
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
exposed-crypt
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
exposed-jdbc
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/Exposed/blob/main/LICENSE.txt
|
||||||
|
|
||||||
|
sqlite-jdbc
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||||
|
|
||||||
|
java-uuid-generator
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/cowtowncoder/java-uuid-generator/blob/master/LICENSE
|
||||||
|
|
||||||
|
semver4j
|
||||||
|
MIT
|
||||||
|
https://github.com/semver4j/semver4j/blob/main/LICENSE
|
||||||
|
|
||||||
|
dom4j
|
||||||
|
Plexus (https://dom4j.github.io)
|
||||||
|
https://github.com/dom4j/dom4j/blob/master/LICENSE
|
||||||
795
build.gradle.kts
@@ -1,24 +1,38 @@
|
|||||||
|
import org.apache.tools.ant.filters.ReplaceTokens
|
||||||
import org.gradle.internal.jvm.Jvm
|
import org.gradle.internal.jvm.Jvm
|
||||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils
|
||||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jetbrains.kotlin.org.apache.commons.lang3.time.DateFormatUtils
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
|
idea
|
||||||
application
|
application
|
||||||
|
`maven-publish`
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
group = "app.termora"
|
group = "app.termora"
|
||||||
version = "1.0.7"
|
version = rootProject.projectDir.resolve("VERSION").readText().trim()
|
||||||
|
|
||||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||||
|
val appVersion = project.version.toString().split("-")[0]
|
||||||
|
val makeAppx = if (os.isWindows) StringUtils.defaultString(System.getenv("MAKEAPPX_PATH")) else StringUtils.EMPTY
|
||||||
|
val isDeb = os.isLinux && System.getenv("TERMORA_TYPE") == "deb"
|
||||||
|
val isAppx = os.isWindows && makeAppx.isNotBlank() && System.getenv("TERMORA_TYPE") == "appx"
|
||||||
|
val isBeta = project.version.toString().contains("beta", ignoreCase = true)
|
||||||
|
|
||||||
// macOS 签名信息
|
// macOS 签名信息
|
||||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||||
@@ -30,15 +44,16 @@ val macOSNotaryKeychainProfile = System.getenv("TERMORA_MAC_NOTARY_KEYCHAIN_PROF
|
|||||||
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
val macOSNotary = macOSSign && macOSNotaryKeychainProfile.isNotBlank()
|
||||||
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
&& System.getenv("TERMORA_MAC_NOTARY").toBoolean()
|
||||||
|
|
||||||
repositories {
|
allprojects {
|
||||||
mavenCentral()
|
repositories {
|
||||||
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
mavenCentral()
|
||||||
maven("https://www.jitpack.io")
|
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
|
||||||
|
maven("https://www.jitpack.io")
|
||||||
|
maven("https://central.sonatype.com/repository/maven-snapshots")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// 由于签名和公证,macOS 不携带 natives
|
|
||||||
val useNoNativesFlatLaf = os.isMacOsX && System.getenv("ENABLE_BUILD").toBoolean()
|
|
||||||
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(libs.hutool)
|
testImplementation(libs.hutool)
|
||||||
@@ -48,153 +63,259 @@ dependencies {
|
|||||||
testImplementation(libs.delight.rhino.sandbox)
|
testImplementation(libs.delight.rhino.sandbox)
|
||||||
testImplementation(platform(libs.testcontainers.bom))
|
testImplementation(platform(libs.testcontainers.bom))
|
||||||
testImplementation(libs.testcontainers)
|
testImplementation(libs.testcontainers)
|
||||||
|
testImplementation(libs.h2)
|
||||||
|
testImplementation(libs.exposed.migration)
|
||||||
|
|
||||||
// implementation(platform(libs.koin.bom))
|
api(kotlin("reflect"))
|
||||||
// implementation(libs.koin.core)
|
api(libs.slf4j.api)
|
||||||
implementation(libs.slf4j.api)
|
api(libs.pty4j)
|
||||||
implementation(libs.pty4j)
|
api(libs.slf4j.tinylog)
|
||||||
implementation(libs.slf4j.tinylog)
|
api(libs.tinylog.impl)
|
||||||
implementation(libs.tinylog.impl)
|
api(libs.commons.codec)
|
||||||
implementation(libs.commons.codec)
|
api(libs.commons.io)
|
||||||
implementation(libs.commons.io)
|
api(libs.commons.lang3)
|
||||||
implementation(libs.commons.lang3)
|
api(libs.commons.csv)
|
||||||
implementation(libs.commons.net)
|
api(libs.commons.net)
|
||||||
implementation(libs.commons.text)
|
api(libs.commons.text)
|
||||||
implementation(libs.commons.compress)
|
api(libs.kotlinx.coroutines.swing)
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
api(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
|
||||||
|
|
||||||
implementation(libs.flatlaf) {
|
api(libs.flatlaf)
|
||||||
artifact {
|
api(libs.flatlafextras)
|
||||||
if (useNoNativesFlatLaf) {
|
api(libs.flatlafswingx)
|
||||||
classifier = "no-natives"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation(libs.flatlaf.extras) {
|
|
||||||
if (useNoNativesFlatLaf) {
|
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation(libs.flatlaf.swingx) {
|
|
||||||
if (useNoNativesFlatLaf) {
|
|
||||||
exclude(group = "com.formdev", module = "flatlaf")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.swingx)
|
api(libs.swingx)
|
||||||
implementation(libs.jgoodies.forms)
|
api(libs.jgoodies.forms)
|
||||||
implementation(libs.jna)
|
api(libs.jna)
|
||||||
implementation(libs.jna.platform)
|
api(libs.jna.platform)
|
||||||
implementation(libs.versioncompare)
|
api(libs.versioncompare)
|
||||||
implementation(libs.oshi.core)
|
api(libs.oshi.core)
|
||||||
implementation(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
api(libs.jSystemThemeDetector) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jfa) { exclude(group = "*", module = "*") }
|
api(libs.jfa) { exclude(group = "*", module = "*") }
|
||||||
implementation(libs.jbr.api)
|
api(libs.jbr.api)
|
||||||
implementation(libs.okhttp)
|
api(libs.okhttp)
|
||||||
implementation(libs.okhttp.logging)
|
api(libs.okhttp.logging)
|
||||||
implementation(libs.sshd.core)
|
api(libs.sshd.core)
|
||||||
implementation(libs.commonmark)
|
api(libs.commonmark)
|
||||||
implementation(libs.jgit)
|
api(libs.jgit)
|
||||||
implementation(libs.jgit.sshd)
|
api(libs.jgit.sshd) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.jnafilechooser)
|
api(libs.jgit.agent) { exclude(group = "*", module = "sshd-osgi") }
|
||||||
implementation(libs.xodus.vfs)
|
api(libs.eddsa)
|
||||||
implementation(libs.xodus.openAPI)
|
api(libs.jnafilechooser)
|
||||||
implementation(libs.xodus.environment)
|
|
||||||
implementation(libs.bip39)
|
api(libs.colorpicker)
|
||||||
implementation(libs.colorpicker)
|
api(libs.mixpanel)
|
||||||
implementation(libs.mixpanel)
|
api(libs.ini4j)
|
||||||
implementation(libs.jSerialComm)
|
api(libs.restart4j)
|
||||||
|
api(libs.exposed.core)
|
||||||
|
api(libs.exposed.crypt)
|
||||||
|
api(libs.exposed.jdbc)
|
||||||
|
api(libs.sqlite)
|
||||||
|
api(libs.jug)
|
||||||
|
api(libs.semver4j)
|
||||||
|
api(libs.jsvg)
|
||||||
|
api(libs.dom4j) { exclude(group = "*", module = "*") }
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
val args = mutableListOf(
|
val args = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
"-Xmx2048m",
|
||||||
"-Xmx2g",
|
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||||
"-XX:+UseZGC",
|
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||||
"-XX:+ZUncommit",
|
|
||||||
"-XX:+ZGenerational",
|
|
||||||
"-XX:ZUncommitDelay=60",
|
|
||||||
"-XX:SoftMaxHeapSize=64m"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
args.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
// macOS NSWindow
|
||||||
|
args.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
args.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
|
args.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
args.add("-Dsun.java2d.metal=true")
|
args.add("-Dsun.java2d.metal=true")
|
||||||
args.add("-Dapple.awt.application.appearance=system")
|
args.add("-Dapple.awt.application.appearance=system")
|
||||||
}
|
}
|
||||||
|
|
||||||
args.add("-Dapp-version=${project.version}")
|
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
||||||
|
|
||||||
if (os.isLinux) {
|
|
||||||
args.add("-Dsun.java2d.opengl=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationDefaultJvmArgs = args
|
applicationDefaultJvmArgs = args
|
||||||
mainClass = "app.termora.MainKt"
|
mainClass = "app.termora.MainKt"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("mavenJava") {
|
||||||
|
from(components["java"])
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name = project.name
|
||||||
|
description = "Termora is a terminal emulator and SSH client for Windows, macOS and Linux"
|
||||||
|
url = "https://github.com/TermoraDev/termora"
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "AGPL-3.0"
|
||||||
|
url = "https://opensource.org/license/agpl-v3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
name = "hstyi"
|
||||||
|
url = "https://github.com/hstyi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scm {
|
||||||
|
url = "https://github.com/TermoraDev/termora"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
val betaVersion = project.version.toString().substringAfterLast('.')
|
||||||
|
filesMatching("**/AppxManifest.xml") {
|
||||||
|
filter<ReplaceTokens>(
|
||||||
|
"tokens" to mapOf(
|
||||||
|
"version" to appVersion,
|
||||||
|
"betaVersion" to if (isBeta) betaVersion else "0",
|
||||||
|
"architecture" to if (arch.isArm64) "arm64" else "x64",
|
||||||
|
"projectDir" to project.projectDir.absolutePath,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
tasks.register<Copy>("copy-dependencies") {
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
val dir = layout.buildDirectory.dir("libs")
|
val dir = layout.buildDirectory.dir("libs")
|
||||||
from(configurations.runtimeClasspath).into(dir)
|
from(configurations.runtimeClasspath).into(dir)
|
||||||
|
val jna = libs.jna.asProvider().get()
|
||||||
|
val pty4j = libs.pty4j.get()
|
||||||
|
val flatlaf = libs.flatlaf.get()
|
||||||
|
val restart4j = libs.restart4j.get()
|
||||||
|
val sqlite = libs.sqlite.get()
|
||||||
|
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||||
|
val dylib = dir.get().dir("dylib").asFile
|
||||||
|
|
||||||
// 对 JNA 和 PTY4J 的本地库提取
|
doLast {
|
||||||
// 提取出来是为了单独签名,不然无法通过公证
|
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||||
if (os.isMacOsX && macOSSign) {
|
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||||
doLast {
|
val targetDir = File(dylib, jna.name)
|
||||||
val jna = libs.jna.asProvider().get()
|
FileUtils.forceMkdir(targetDir)
|
||||||
val dylib = dir.get().dir("dylib").asFile
|
if (os.isWindows) {
|
||||||
val pty4j = libs.pty4j.get()
|
// @formatter:off
|
||||||
val jSerialComm = libs.jSerialComm.get()
|
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/win32-${arch.name}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
} else if (os.isLinux) {
|
||||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
// @formatter:off
|
||||||
val targetDir = File(dylib, jna.name)
|
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/linux-${arch.name}/*", "-d", targetDir.absolutePath) }
|
||||||
FileUtils.forceMkdir(targetDir)
|
// @formatter:on
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
|
exec { commandLine("unzip","-j","-o", file.absolutePath, "com/sun/jna/darwin-${arch.name}/*", "-d", targetDir.absolutePath) }
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
// 删除所有二进制类库
|
}
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/darwin-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/sunos-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/openbsd-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/freebsd-*") }
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/dragonflybsd-*") }
|
||||||
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") }
|
||||||
val targetDir = FileUtils.getFile(dylib, pty4j.name, "darwin")
|
} else if ("${pty4j.name}-${pty4j.version}" == file.nameWithoutExtension) {
|
||||||
FileUtils.forceMkdir(targetDir)
|
val osName = if (os.isWindows) "win32" else if (os.isMacOsX) "darwin" else "linux"
|
||||||
|
val myArchName = if (arch.isArm) "aarch64" else "x86-64"
|
||||||
|
val targetDir = if (os.isMacOsX) FileUtils.getFile(dylib, pty4j.name, osName)
|
||||||
|
else FileUtils.getFile(dylib, pty4j.name, osName, myArchName)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
if (os.isWindows) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*win/${myArchName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/*linux/${myArchName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "resources/com/pty4j/native/darwin*", "-d", targetDir.absolutePath) }
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
// 删除所有二进制类库
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
|
||||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
|
||||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
|
||||||
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
|
||||||
FileUtils.forceMkdir(targetDir)
|
|
||||||
// @formatter:off
|
|
||||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "OSX/${archName}/*", "-d", targetDir.absolutePath) }
|
|
||||||
// @formatter:on
|
|
||||||
// 删除所有二进制类库
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
|
||||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
|
||||||
}
|
}
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||||
|
} else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, restart4j.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
if (os.isWindows) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "win32/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "linux/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
// 设置可执行权限
|
||||||
|
for (e in FileUtils.listFiles(
|
||||||
|
targetDir,
|
||||||
|
FileFilterUtils.trueFileFilter(),
|
||||||
|
FileFilterUtils.falseFileFilter()
|
||||||
|
)) e.setExecutable(true)
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "win32/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") }
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "linux/*") }
|
||||||
|
} else if ("${sqlite.name}-${sqlite.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, sqlite.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
if (os.isWindows) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Windows/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Linux/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "org/sqlite/native/Mac/${archName}/*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "org/sqlite/native/*") }
|
||||||
|
} else if ("${flatlaf.name}-${flatlaf.version}" == file.nameWithoutExtension) {
|
||||||
|
val targetDir = FileUtils.getFile(dylib, flatlaf.name)
|
||||||
|
FileUtils.forceMkdir(targetDir)
|
||||||
|
val isArm = arch.isArm
|
||||||
|
if (os.isWindows) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*windows*${if (isArm) "arm64" else "x86_64"}*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*linux*${if (isArm) "arm64" else "x86_64"}*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
} else if (os.isMacOsX) {
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "com/formdev/flatlaf/natives/*macos*${if (isArm) "arm" else "x86"}*", "-d", targetDir.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
exec { commandLine("zip", "-d", file.absolutePath, "com/formdev/flatlaf/natives/*") }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 对二进制签名
|
// 对二进制签名
|
||||||
|
if (os.isMacOsX) {
|
||||||
Files.walk(dylib.toPath()).use { paths ->
|
Files.walk(dylib.toPath()).use { paths ->
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
if (Files.isRegularFile(path)) {
|
if (Files.isRegularFile(path)) {
|
||||||
@@ -204,6 +325,7 @@ tasks.register<Copy>("copy-dependencies") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Exec>("jlink") {
|
tasks.register<Exec>("jlink") {
|
||||||
@@ -213,9 +335,11 @@ tasks.register<Exec>("jlink") {
|
|||||||
"java.logging",
|
"java.logging",
|
||||||
"java.management",
|
"java.management",
|
||||||
"java.rmi",
|
"java.rmi",
|
||||||
|
"java.sql",
|
||||||
"java.security.jgss",
|
"java.security.jgss",
|
||||||
"jdk.crypto.ec",
|
"jdk.crypto.ec",
|
||||||
"jdk.unsupported",
|
"jdk.unsupported",
|
||||||
|
"jdk.httpserver",
|
||||||
)
|
)
|
||||||
|
|
||||||
commandLine(
|
commandLine(
|
||||||
@@ -238,34 +362,39 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
val buildDir = layout.buildDirectory.get()
|
val buildDir = layout.buildDirectory.get()
|
||||||
val options = mutableListOf(
|
val options = mutableListOf(
|
||||||
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
"-Xmx2048m",
|
||||||
"-Xmx2g",
|
|
||||||
"-XX:+UseZGC",
|
|
||||||
"-XX:+ZUncommit",
|
|
||||||
"-XX:+ZGenerational",
|
|
||||||
"-XX:ZUncommitDelay=60",
|
|
||||||
"-XX:SoftMaxHeapSize=64m",
|
|
||||||
"-XX:+HeapDumpOnOutOfMemoryError",
|
"-XX:+HeapDumpOnOutOfMemoryError",
|
||||||
"-Dlogger.console.level=off",
|
"-Dlogger.console.level=off",
|
||||||
"-Dkotlinx.coroutines.debug=off",
|
"-Dkotlinx.coroutines.debug=off",
|
||||||
"-Dapp-version=${project.version}",
|
"-Dapp-version=${project.version}",
|
||||||
|
"-Drelease-date=${DateFormatUtils.format(Date(), "yyyy-MM-dd")}",
|
||||||
|
"--add-exports java.base/sun.nio.ch=ALL-UNNAMED",
|
||||||
)
|
)
|
||||||
|
|
||||||
options.add("-Dsun.java2d.metal=true")
|
options.add("-Dsun.java2d.metal=true")
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
|
// NSWindow
|
||||||
options.add("-Dapple.awt.application.appearance=system")
|
options.add("-Dapple.awt.application.appearance=system")
|
||||||
|
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
options.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.isLinux) {
|
if (os.isLinux) {
|
||||||
options.add("-Dsun.java2d.opengl=true")
|
options.add("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED")
|
||||||
|
if (isDeb) {
|
||||||
|
options.add("-Djpackage.app-layout=deb")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
val arguments = mutableListOf("${Jvm.current().javaHome}/bin/jpackage")
|
||||||
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
arguments.addAll(listOf("--runtime-image", "${buildDir}/jlink"))
|
||||||
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--name", project.name.uppercaseFirstChar()))
|
||||||
arguments.addAll(listOf("--app-version", "${project.version}"))
|
arguments.addAll(listOf("--app-version", appVersion))
|
||||||
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
|
arguments.addAll(listOf("--main-jar", tasks.jar.get().archiveFileName.get()))
|
||||||
arguments.addAll(listOf("--main-class", application.mainClass.get()))
|
arguments.addAll(listOf("--main-class", application.mainClass.get()))
|
||||||
arguments.addAll(listOf("--input", "$buildDir/libs"))
|
arguments.addAll(listOf("--input", "$buildDir/libs"))
|
||||||
@@ -274,8 +403,7 @@ tasks.register<Exec>("jpackage") {
|
|||||||
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
arguments.addAll(listOf("--java-options", options.joinToString(StringUtils.SPACE)))
|
||||||
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
arguments.addAll(listOf("--vendor", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
arguments.addAll(listOf("--copyright", "TermoraDev"))
|
||||||
arguments.addAll(listOf("--description", "A terminal emulator and SSH client."))
|
arguments.addAll(listOf("--app-content", "$buildDir/plugins"))
|
||||||
|
|
||||||
|
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
arguments.addAll(listOf("--mac-package-name", project.name.uppercaseFirstChar()))
|
||||||
@@ -285,26 +413,30 @@ tasks.register<Exec>("jpackage") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (os.isWindows) {
|
if (os.isWindows) {
|
||||||
arguments.add("--win-dir-chooser")
|
|
||||||
arguments.add("--win-shortcut")
|
|
||||||
arguments.add("--win-shortcut-prompt")
|
|
||||||
arguments.addAll(listOf("--win-upgrade-uuid", "E1D93CAD-5BF8-442E-93BA-6E90DE601E4C"))
|
|
||||||
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.ico"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (os.isLinux) {
|
||||||
|
arguments.addAll(listOf("--icon", "${projectDir.absolutePath}/src/main/resources/icons/termora.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
arguments.add("--type")
|
arguments.add("--type")
|
||||||
if (os.isMacOsX) {
|
if (os.isMacOsX) {
|
||||||
arguments.add("dmg")
|
arguments.add("dmg")
|
||||||
} else if (os.isWindows) {
|
} else if (os.isWindows) {
|
||||||
arguments.add("msi")
|
|
||||||
} else if (os.isLinux) {
|
|
||||||
arguments.add("app-image")
|
arguments.add("app-image")
|
||||||
|
} else if (os.isLinux) {
|
||||||
|
arguments.add(if (isDeb) "deb" else "app-image")
|
||||||
|
if (isDeb) {
|
||||||
|
arguments.add("--linux-deb-maintainer")
|
||||||
|
arguments.add("support@termora.app")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (os.isMacOsX && macOSSign) {
|
if (macOSSign) {
|
||||||
arguments.add("--mac-sign")
|
arguments.add("--mac-sign")
|
||||||
arguments.add("--mac-signing-key-user-name")
|
arguments.add("--mac-signing-key-user-name")
|
||||||
arguments.add(macOSSignUsername)
|
arguments.add(macOSSignUsername)
|
||||||
@@ -316,142 +448,270 @@ tasks.register<Exec>("jpackage") {
|
|||||||
|
|
||||||
tasks.register("dist") {
|
tasks.register("dist") {
|
||||||
doLast {
|
doLast {
|
||||||
val vendor = Jvm.current().vendor ?: StringUtils.EMPTY
|
|
||||||
@Suppress("UnstableApiUsage")
|
|
||||||
if (!JvmVendorSpec.JETBRAINS.matches(vendor)) {
|
|
||||||
throw GradleException("JVM: $vendor is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
|
||||||
val gradlew = File(projectDir, if (os.isWindows) "gradlew.bat" else "gradlew").absolutePath
|
|
||||||
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
val osName = if (os.isMacOsX) "osx" else if (os.isWindows) "windows" else "linux"
|
||||||
|
val distributionDir = layout.buildDirectory.dir("distributions").get()
|
||||||
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
val finalFilenameWithoutExtension = "${project.name}-${project.version}-${osName}-${arch.name}"
|
||||||
val macOSFinalFilePath = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile.absolutePath
|
val projectName = project.name.uppercaseFirstChar()
|
||||||
|
|
||||||
// 清空目录
|
if (os.isWindows) {
|
||||||
exec { commandLine(gradlew, "clean") }
|
packOnWindows(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
|
} else if (os.isLinux) {
|
||||||
// 打包并复制依赖
|
packOnLinux(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
exec {
|
} else if (os.isMacOsX) {
|
||||||
commandLine(gradlew, "jar", "copy-dependencies")
|
packOnMac(distributionDir, finalFilenameWithoutExtension, projectName)
|
||||||
environment("ENABLE_BUILD" to true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查依赖的开源协议
|
|
||||||
exec { commandLine(gradlew, "check-license") }
|
|
||||||
|
|
||||||
// jlink
|
|
||||||
exec { commandLine(gradlew, "jlink") }
|
|
||||||
|
|
||||||
// 打包
|
|
||||||
exec { commandLine(gradlew, "jpackage") }
|
|
||||||
|
|
||||||
// pack
|
|
||||||
if (os.isWindows) { // zip and msi
|
|
||||||
// zip
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"tar", "-vacf",
|
|
||||||
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
|
||||||
project.name.uppercaseFirstChar()
|
|
||||||
)
|
|
||||||
workingDir = layout.buildDirectory.dir("jpackage/images/win-msi.image/").get().asFile
|
|
||||||
}
|
|
||||||
|
|
||||||
// msi
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"cmd", "/c", "move",
|
|
||||||
"${project.name.uppercaseFirstChar()}-${project.version}.msi",
|
|
||||||
"${finalFilenameWithoutExtension}.msi"
|
|
||||||
)
|
|
||||||
workingDir = distributionDir.asFile
|
|
||||||
}
|
|
||||||
} else if (os.isLinux) { // tar.gz
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"tar", "-czvf",
|
|
||||||
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
|
||||||
project.name.uppercaseFirstChar()
|
|
||||||
)
|
|
||||||
workingDir = distributionDir.asFile
|
|
||||||
}
|
|
||||||
} else if (os.isMacOsX) { // rename
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"mv",
|
|
||||||
distributionDir.file("${project.name.uppercaseFirstChar()}-${project.version}.dmg").asFile.absolutePath,
|
|
||||||
macOSFinalFilePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw GradleException("${os.name} is not supported")
|
throw GradleException("${os.name} is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// sign dmg
|
|
||||||
if (os.isMacOsX && macOSSign) {
|
|
||||||
|
|
||||||
// sign
|
|
||||||
signMacOSLocalFile(File(macOSFinalFilePath))
|
|
||||||
|
|
||||||
// notary
|
|
||||||
if (macOSNotary) {
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"/usr/bin/xcrun", "notarytool",
|
|
||||||
"submit", macOSFinalFilePath,
|
|
||||||
"--keychain-profile", macOSNotaryKeychainProfile,
|
|
||||||
"--wait",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定公证信息
|
|
||||||
exec {
|
|
||||||
commandLine(
|
|
||||||
"/usr/bin/xcrun",
|
|
||||||
"stapler", "staple", macOSFinalFilePath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("check-license") {
|
tasks.register("check-license") {
|
||||||
doLast {
|
doLast {
|
||||||
val thirdParty = mutableMapOf<String, String>()
|
|
||||||
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
val iterator = File(projectDir, "THIRDPARTY").readLines().iterator()
|
||||||
val thirdPartyNames = mutableSetOf<String>()
|
val thirdPartyNames = mutableSetOf<String>()
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val nameWithVersion = iterator.next()
|
val name = iterator.next()
|
||||||
if (nameWithVersion.isBlank()) {
|
if (name.isBlank()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore license name
|
// ignore license name
|
||||||
iterator.next()
|
iterator.next()
|
||||||
|
// ignore license url
|
||||||
|
iterator.next()
|
||||||
|
|
||||||
val license = iterator.next()
|
thirdPartyNames.add(name)
|
||||||
thirdParty[nameWithVersion.replace(StringUtils.SPACE, "-")] = license
|
|
||||||
thirdPartyNames.add(nameWithVersion.split(StringUtils.SPACE).first())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (file in configurations.runtimeClasspath.get()) {
|
for (dependency in configurations.runtimeClasspath.get().allDependencies) {
|
||||||
val name = file.nameWithoutExtension
|
if (!thirdPartyNames.contains(dependency.name)) {
|
||||||
if (!thirdParty.containsKey(name)) {
|
throw GradleException("${dependency.name} No license found")
|
||||||
if (logger.isWarnEnabled) {
|
|
||||||
logger.warn("$name does not exist in third-party")
|
|
||||||
}
|
|
||||||
if (!thirdPartyNames.contains(name)) {
|
|
||||||
throw GradleException("$name No license found")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 zip、msi
|
||||||
|
*/
|
||||||
|
fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
val dir = layout.buildDirectory.dir("distributions").get().asFile
|
||||||
|
val cfg = FileUtils.getFile(dir, projectName, "app", "${projectName}.cfg")
|
||||||
|
val configText = cfg.readText()
|
||||||
|
|
||||||
|
// appx
|
||||||
|
if (isAppx) {
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=appx").toString())
|
||||||
|
val appxManifest = FileUtils.getFile(dir, projectName, "AppxManifest.xml")
|
||||||
|
layout.buildDirectory.file("resources/main/AppxManifest.xml").get().asFile
|
||||||
|
.renameTo(appxManifest)
|
||||||
|
val icons = setOf("termora.png", "termora_44x44.png", "termora_150x150.png")
|
||||||
|
for (file in projectDir.resolve("src/main/resources/icons/").listFiles()) {
|
||||||
|
if (icons.contains(file.name)) {
|
||||||
|
val p = appxManifest.parentFile.resolve("icons/${file.name}")
|
||||||
|
FileUtils.forceMkdirParent(p)
|
||||||
|
file.copyTo(p, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exec {
|
||||||
|
commandLine(makeAppx, "pack", "/d", projectName, "/p", "${finalFilenameWithoutExtension}.msix")
|
||||||
|
workingDir = dir
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// zip
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=zip").toString())
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"tar", "-vacf",
|
||||||
|
distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile.absolutePath,
|
||||||
|
projectName
|
||||||
|
)
|
||||||
|
workingDir = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// exe
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=exe").toString())
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"iscc",
|
||||||
|
"/DMyAppId=${projectName}",
|
||||||
|
"/DMyAppName=${projectName}",
|
||||||
|
"/DMyAppVersion=${appVersion}",
|
||||||
|
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",
|
||||||
|
"/DMySetupIconFile=${FileUtils.getFile(projectDir, "src", "main", "resources", "icons", "termora.ico")}",
|
||||||
|
"/DMyWizardSmallImageFile=${
|
||||||
|
FileUtils.getFile(
|
||||||
|
projectDir,
|
||||||
|
"src",
|
||||||
|
"main",
|
||||||
|
"resources",
|
||||||
|
"icons",
|
||||||
|
"termora_128x128.bmp"
|
||||||
|
)
|
||||||
|
}",
|
||||||
|
"/DMySourceDir=${FileUtils.getFile(dir, projectName).absolutePath}",
|
||||||
|
"/F${finalFilenameWithoutExtension}",
|
||||||
|
FileUtils.getFile(projectDir, "src", "main", "resources", "termora.iss")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对于 macOS 先对 jpackage 构建的 dmg 重命名 -> 签名 -> 公证,另外还会创建一个 zip 包
|
||||||
|
*/
|
||||||
|
fun packOnMac(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
val dmgFile = distributionDir.file("${finalFilenameWithoutExtension}.dmg").asFile
|
||||||
|
val zipFile = distributionDir.file("${finalFilenameWithoutExtension}.zip").asFile
|
||||||
|
|
||||||
|
// rename
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("mv", distributionDir.file("${projectName}-${appVersion}.dmg").asFile.absolutePath, dmgFile.absolutePath,) }
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// sign dmg
|
||||||
|
signMacOSLocalFile(dmgFile)
|
||||||
|
|
||||||
|
// 找到 .app
|
||||||
|
val imageFile = layout.buildDirectory.dir("jpackage/images/").get().asFile
|
||||||
|
val appFile = imageFile.listFiles()?.firstOrNull()?.listFiles()?.firstOrNull()
|
||||||
|
?: throw FileNotFoundException("${projectName}.app")
|
||||||
|
|
||||||
|
// zip
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
// sign zip
|
||||||
|
signMacOSLocalFile(zipFile)
|
||||||
|
|
||||||
|
// 公证
|
||||||
|
if (macOSNotary) {
|
||||||
|
val pool = Executors.newCachedThreadPool()
|
||||||
|
val jobs = mutableListOf<Future<*>>()
|
||||||
|
|
||||||
|
// zip
|
||||||
|
pool.submit {
|
||||||
|
// 对 zip 公证
|
||||||
|
notaryMacOSLocalFile(zipFile)
|
||||||
|
// 对 .app 盖章
|
||||||
|
stapleMacOSLocalFile(appFile)
|
||||||
|
// 删除旧的 zip ,旧的 zip 仅仅是为了公证
|
||||||
|
FileUtils.deleteQuietly(zipFile)
|
||||||
|
// 再对盖完章的 app 打成 zip 包
|
||||||
|
// @formatter:off
|
||||||
|
exec { commandLine("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", appFile.absolutePath, zipFile.absolutePath) }
|
||||||
|
// @formatter:on
|
||||||
|
// 再对 zip 签名
|
||||||
|
signMacOSLocalFile(zipFile)
|
||||||
|
}.apply { jobs.add(this) }
|
||||||
|
|
||||||
|
// dmg
|
||||||
|
pool.submit {
|
||||||
|
// 公证
|
||||||
|
notaryMacOSLocalFile(dmgFile)
|
||||||
|
// 盖章
|
||||||
|
stapleMacOSLocalFile(dmgFile)
|
||||||
|
}.apply { jobs.add(this) }
|
||||||
|
|
||||||
|
// join ...
|
||||||
|
jobs.forEach { it.get() }
|
||||||
|
|
||||||
|
// shutdown
|
||||||
|
pool.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 tar.gz 和 AppImage
|
||||||
|
*/
|
||||||
|
fun packOnLinux(distributionDir: Directory, finalFilenameWithoutExtension: String, projectName: String) {
|
||||||
|
|
||||||
|
if (isDeb) {
|
||||||
|
val arch = if (arch.isArm) "arm" else "amd"
|
||||||
|
distributionDir.file("${project.name}_${appVersion}_${arch}64.deb").asFile
|
||||||
|
.renameTo(distributionDir.file("${finalFilenameWithoutExtension}.deb").asFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cfg = FileUtils.getFile(distributionDir.asFile, projectName, "lib", "app", "${projectName}.cfg")
|
||||||
|
val configText = cfg.readText()
|
||||||
|
|
||||||
|
// tar.gz
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=tar.gz").toString())
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"tar", "-czvf",
|
||||||
|
distributionDir.file("${finalFilenameWithoutExtension}.tar.gz").asFile.absolutePath,
|
||||||
|
projectName
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AppImage
|
||||||
|
// Download AppImageKit
|
||||||
|
val appimagetool = FileUtils.getFile(projectDir, ".gradle", "appimagetool")
|
||||||
|
if (!appimagetool.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"wget",
|
||||||
|
"-O", appimagetool.absolutePath,
|
||||||
|
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${if (arch.isArm) "aarch64" else "x86_64"}.AppImage"
|
||||||
|
)
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppImageKit chmod
|
||||||
|
exec { commandLine("chmod", "+x", appimagetool.absolutePath) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop file
|
||||||
|
val termoraName = project.name.uppercaseFirstChar()
|
||||||
|
|
||||||
|
// copy icon
|
||||||
|
FileUtils.copyFile(
|
||||||
|
File("${projectDir.absolutePath}/src/main/resources/icons/termora_256x256.png"),
|
||||||
|
distributionDir.file(termoraName + File.separator + termoraName + ".png").asFile
|
||||||
|
)
|
||||||
|
|
||||||
|
val desktopFile = distributionDir.file(termoraName + File.separator + termoraName + ".desktop").asFile
|
||||||
|
desktopFile.writeText(
|
||||||
|
"""[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=${termoraName}
|
||||||
|
Comment=Terminal emulator and SSH client
|
||||||
|
Icon=${termoraName}
|
||||||
|
Categories=Development;
|
||||||
|
StartupWMClass=${termoraName}
|
||||||
|
Terminal=false
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppRun file
|
||||||
|
val appRun = File(desktopFile.parentFile, "AppRun")
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("#!/bin/sh").appendLine()
|
||||||
|
sb.append("SELF=$(readlink -f \"$0\")").appendLine()
|
||||||
|
sb.append("HERE=\${SELF%/*}").appendLine()
|
||||||
|
sb.append("export LinuxAppImage=true").appendLine()
|
||||||
|
sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"")
|
||||||
|
appRun.writeText(sb.toString())
|
||||||
|
appRun.setExecutable(true)
|
||||||
|
|
||||||
|
// AppImage
|
||||||
|
cfg.writeText(StringBuilder(configText).appendLine("java-options=-Djpackage.app-layout=AppImage").toString())
|
||||||
|
exec {
|
||||||
|
commandLine(appimagetool.absolutePath, termoraName, "${finalFilenameWithoutExtension}.AppImage")
|
||||||
|
workingDir = distributionDir.asFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* macOS 对本地文件进行签名
|
* macOS 对本地文件进行签名
|
||||||
*/
|
*/
|
||||||
@@ -471,11 +731,54 @@ fun signMacOSLocalFile(file: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* macOS 对本地文件进行公证
|
||||||
|
*/
|
||||||
|
fun notaryMacOSLocalFile(file: File) {
|
||||||
|
if (os.isMacOsX && macOSNotary) {
|
||||||
|
if (file.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun", "notarytool",
|
||||||
|
"submit", file,
|
||||||
|
"--keychain-profile", macOSNotaryKeychainProfile,
|
||||||
|
"--wait",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盖章
|
||||||
|
*/
|
||||||
|
fun stapleMacOSLocalFile(file: File) {
|
||||||
|
if (os.isMacOsX && macOSNotary) {
|
||||||
|
if (file.exists()) {
|
||||||
|
exec {
|
||||||
|
commandLine(
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"stapler", "staple", file,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain {
|
jvmToolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
@Suppress("UnstableApiUsage")
|
}
|
||||||
vendor = JvmVendorSpec.JETBRAINS
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
idea {
|
||||||
|
module {
|
||||||
|
isDownloadJavadoc = true
|
||||||
|
isDownloadSources = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
docs/host-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/plugins-zh_CN.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
|
Before Width: | Height: | Size: 49 KiB |
BIN
docs/tags-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/transfer-edit-zh_CN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/transfer-edit.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/transfer-zh_CN.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/transfer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -1,47 +1,53 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.0"
|
kotlin = "2.3.0"
|
||||||
slf4j = "2.0.16"
|
slf4j = "2.0.17"
|
||||||
pty4j = "0.13.2"
|
pty4j = "0.13.10"
|
||||||
tinylog = "2.7.0"
|
tinylog = "2.7.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.2"
|
||||||
flatlaf = "3.5.4"
|
flatlaf = "3.7"
|
||||||
trove4j = "1.0.20200330"
|
kotlinx-serialization-json = "1.9.0"
|
||||||
kotlinx-serialization-json = "1.7.3"
|
commons-codec = "1.20.0"
|
||||||
commons-codec = "1.17.1"
|
commons-lang3 = "3.20.0"
|
||||||
commons-lang3 = "3.17.0"
|
commons-csv = "1.14.1"
|
||||||
commons-net = "3.11.1"
|
commons-net = "3.12.0"
|
||||||
commons-text = "1.12.0"
|
commons-text = "1.15.0"
|
||||||
commons-compress = "1.27.1"
|
commons-compress = "1.28.0"
|
||||||
koin-bom = "4.0.0"
|
commons-vfs2 = "2.10.0"
|
||||||
swingx = "1.6.5-1"
|
swingx = "1.6.5-1"
|
||||||
jgoodies-forms = "1.9.0"
|
jgoodies-forms = "1.9.0"
|
||||||
jfa = "1.2.0"
|
jfa = "1.2.0"
|
||||||
oshi = "6.6.5"
|
oshi = "6.9.1"
|
||||||
versioncompare = "1.4.1"
|
versioncompare = "1.4.1"
|
||||||
jna = "5.16.0"
|
jna = "5.18.1"
|
||||||
jSystemThemeDetector = "3.9.1"
|
jSystemThemeDetector = "3.9.1"
|
||||||
commons-io = "2.18.0"
|
commons-io = "2.21.0"
|
||||||
jbr-api = "17.1.10.1"
|
jbr-api = "17.1.10.1"
|
||||||
leveldb = "0.12"
|
hutool = "5.8.40"
|
||||||
guava = "33.3.1-jre"
|
jsch = "2.27.3"
|
||||||
credential-secure-storage = "1.0.3"
|
okhttp = "5.3.0"
|
||||||
hutool = "5.8.34"
|
|
||||||
jsch = "0.2.21"
|
|
||||||
okhttp = "4.12.0"
|
|
||||||
bcprov = "1.79"
|
|
||||||
sshj = "0.39.0"
|
sshj = "0.39.0"
|
||||||
sshd-core = "2.14.0"
|
sshd-core = "2.15.0"
|
||||||
jgit = "7.1.0.202411261347-r"
|
jgit = "7.4.0.202509020913-r"
|
||||||
commonmark = "0.24.0"
|
commonmark = "0.27.0"
|
||||||
jnafilechooser = "1.1.2"
|
jnafilechooser = "1.1.2"
|
||||||
xodus = "2.0.1"
|
xodus = "2.0.1"
|
||||||
bip39 = "1.0.8"
|
bip39 = "1.0.9"
|
||||||
colorpicker = "2.0.1"
|
colorpicker = "2.0.1"
|
||||||
rhino = "1.7.15"
|
rhino = "1.8.0"
|
||||||
delight-rhino-sandbox = "0.0.17"
|
delight-rhino-sandbox = "0.2.1"
|
||||||
testcontainers = "1.20.4"
|
testcontainers = "2.0.3"
|
||||||
mixpanel = "1.5.3"
|
mixpanel = "1.5.4"
|
||||||
jSerialComm="2.11.0"
|
jSerialComm = "2.11.4"
|
||||||
|
ini4j = "0.5.5-2"
|
||||||
|
restart4j = "0.0.1"
|
||||||
|
eddsa = "0.3.0"
|
||||||
|
exposed = "1.0.0-rc-4"
|
||||||
|
h2 = "2.3.232"
|
||||||
|
sqlite = "3.50.3.0"
|
||||||
|
jug = "5.2.0"
|
||||||
|
semver4j = "6.0.0"
|
||||||
|
jsvg = "2.0.0"
|
||||||
|
dom4j = "2.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||||
@@ -53,16 +59,18 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti
|
|||||||
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
|
||||||
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" }
|
||||||
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
|
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
|
||||||
|
commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" }
|
||||||
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" }
|
||||||
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" }
|
||||||
|
commons-vfs2 = { group = "org.apache.commons", name = "commons-vfs2", version.ref = "commons-vfs2" }
|
||||||
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" }
|
||||||
|
ini4j = { module = "org.jetbrains.intellij.deps:ini4j", version.ref = "ini4j" }
|
||||||
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
flatlaf = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" }
|
||||||
flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" }
|
||||||
trove4j = { group = "org.jetbrains.intellij.deps", name = "trove4j", version.ref = "trove4j" }
|
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
||||||
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
|
||||||
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
|
||||||
testcontainers = { module = "org.testcontainers:testcontainers" }
|
testcontainers = { module = "org.testcontainers:testcontainers" }
|
||||||
koin-core = { module = "io.insert-koin:koin-core" }
|
testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter" }
|
||||||
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
|
||||||
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
|
||||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||||
@@ -72,33 +80,39 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve
|
|||||||
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
jfa = { module = "de.jangassen:jfa", version.ref = "jfa" }
|
||||||
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" }
|
||||||
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
|
||||||
|
restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" }
|
||||||
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" }
|
||||||
flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
|
|
||||||
leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" }
|
|
||||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
|
||||||
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
hutool = { module = "cn.hutool:hutool-all", version.ref = "hutool" }
|
||||||
credential-secure-storage = { module = "com.microsoft:credential-secure-storage", version.ref = "credential-secure-storage" }
|
|
||||||
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
jsch = { module = "com.github.mwiede:jsch", version.ref = "jsch" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
||||||
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
|
|
||||||
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" }
|
||||||
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
sshd-core = { module = "org.apache.sshd:sshd-core", version.ref = "sshd-core" }
|
||||||
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" }
|
||||||
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
|
||||||
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
jgit-sshd = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", version.ref = "jgit" }
|
||||||
|
jgit-agent = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent", version.ref = "jgit" }
|
||||||
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
|
xodus-openAPI = { module = "org.jetbrains.xodus:xodus-openAPI", version.ref = "xodus" }
|
||||||
xodus-entity-store = { module = "org.jetbrains.xodus:xodus-entity-store", version.ref = "xodus" }
|
|
||||||
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
|
xodus-environment = { module = "org.jetbrains.xodus:xodus-environment", version.ref = "xodus" }
|
||||||
xodus-crypto = { module = "org.jetbrains.xodus:xodus-crypto", version.ref = "xodus" }
|
|
||||||
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
xodus-vfs = { module = "org.jetbrains.xodus:xodus-vfs", version.ref = "xodus" }
|
||||||
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
jnafilechooser = { module = "com.github.steos.jnafilechooser:jnafilechooser-api", version.ref = "jnafilechooser" }
|
||||||
bip39 = { module = "cash.z.ecc.android:kotlin-bip39-jvm", version.ref = "bip39" }
|
bip39 = { module = "cash.z.ecc.android:kotlin-bip39", version.ref = "bip39" }
|
||||||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||||
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||||
|
eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
|
||||||
|
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
|
||||||
|
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
|
||||||
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
|
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration-core", version.ref = "exposed" }
|
||||||
|
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||||
|
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
|
||||||
|
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }
|
||||||
|
jsvg = { module = "com.github.weisj:jsvg", version.ref = "jsvg" }
|
||||||
|
dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" }
|
||||||
|
semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
11
plugins/LICENSE
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Copyright (c) 2025-present hstyi
|
||||||
|
|
||||||
|
The files in this catalogue are for public access only. Specific descriptions are given below:
|
||||||
|
|
||||||
|
- You may view and study the contents of these files;
|
||||||
|
- You may NOT use them for any commercial purpose;
|
||||||
|
- You may NOT modify, copy, distribute, republish, or use them to create derivative works;
|
||||||
|
- Written permission must be obtained from the author for any use beyond personal viewing;
|
||||||
|
- If you submit a Pull Request that modifies, supplements, or adds to the files in this directory or its subdirectories, unless otherwise agreed in writing, you agree that the copyright of your contribution is owned by hstyi and may be used and managed under the current license terms.
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
79
plugins/THIRDPARTY
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
minio
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/minio/minio-java/blob/master/LICENSE
|
||||||
|
|
||||||
|
aliyun-sdk-oss
|
||||||
|
Apache License 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
jaxb-api
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/jakartaee/jaxb-api/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
activation
|
||||||
|
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
|
||||||
|
https://github.com/javaee/activation/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
jaxb-runtime
|
||||||
|
BSD 3-Clause "New" or "Revised" License
|
||||||
|
https://github.com/eclipse-ee4j/jaxb-ri/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
esdk-obs-java-bundle
|
||||||
|
HUAWEI LICENSE
|
||||||
|
https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/master/LICENSE
|
||||||
|
|
||||||
|
xodus-compress
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-environment
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-openAPI
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-utils
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
xodus-vfs
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/JetBrains/xodus/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
kotlin-bip39
|
||||||
|
MIT License
|
||||||
|
https://github.com/Electric-Coin-Company/kotlin-bip39/blob/main/LICENSE
|
||||||
|
|
||||||
|
commons-compress
|
||||||
|
Apache License 2.0
|
||||||
|
https://github.com/apache/commons-compress/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
cos_api
|
||||||
|
MIT License
|
||||||
|
https://github.com/tencentyun/cos-java-sdk-v5/blob/master/LICENSE
|
||||||
|
|
||||||
|
AutoComplete
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/AutoComplete/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
RSTALanguageSupport
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/RSTALanguageSupport/blob/master/README.md
|
||||||
|
|
||||||
|
RSyntaxTextArea
|
||||||
|
BSD-3-Clause license
|
||||||
|
https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
|
||||||
|
|
||||||
|
MaxMind GeoIP2 API
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
GeoLite2 (https://www.maxmind.com)
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||||
|
https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
|
||||||
|
smbj
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER
|
||||||
16
plugins/bg/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.6"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
|
||||||
|
object Appearance {
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||||
|
|
||||||
|
var backgroundImage: String
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.backgroundImage", appearance.backgroundImage)
|
||||||
|
set(value) {
|
||||||
|
enableManager.setFlag("Plugins.bg.backgroundImage", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var interval: Int
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.interval", 360)
|
||||||
|
set(value) {
|
||||||
|
enableManager.setFlag("Plugins.bg.interval", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fillMode: String
|
||||||
|
get() = enableManager.getFlag("Plugins.bg.fillMode", FillMode.STRETCH.name)
|
||||||
|
set(value) = enableManager.setFlag("Plugins.bg.fillMode", value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.GlassPaneExtension
|
||||||
|
import app.termora.WindowScope
|
||||||
|
import app.termora.restore
|
||||||
|
import app.termora.save
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import java.awt.AlphaComposite
|
||||||
|
import java.awt.Graphics2D
|
||||||
|
import javax.swing.JComponent
|
||||||
|
|
||||||
|
class BGGlassPaneExtension private constructor() : GlassPaneExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = BGGlassPaneExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun paint(scope: WindowScope, c: JComponent, g2d: Graphics2D) {
|
||||||
|
|
||||||
|
val img = BackgroundManager.getInstance().getBackgroundImage() ?: return
|
||||||
|
g2d.save()
|
||||||
|
g2d.composite = AlphaComposite.getInstance(
|
||||||
|
AlphaComposite.SRC_OVER,
|
||||||
|
if (FlatLaf.isLafDark()) 0.2f else 0.1f
|
||||||
|
)
|
||||||
|
|
||||||
|
when (Appearance.fillMode) {
|
||||||
|
FillMode.STRETCH.name -> {
|
||||||
|
g2d.drawImage(img, 0, 0, c.width, c.height, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
FillMode.CENTER.name -> {
|
||||||
|
val x = (c.width - img.width) / 2
|
||||||
|
val y = (c.height - img.height) / 2
|
||||||
|
g2d.drawImage(img, x, y, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
FillMode.TILE.name -> {
|
||||||
|
val iw = img.width
|
||||||
|
val ih = img.height
|
||||||
|
var y = 0
|
||||||
|
while (y < c.height) {
|
||||||
|
var x = 0
|
||||||
|
while (x < c.width) {
|
||||||
|
g2d.drawImage(img, x, y, null)
|
||||||
|
x += iw
|
||||||
|
}
|
||||||
|
y += ih
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FillMode.FIT.name -> {
|
||||||
|
val scale = maxOf(c.width.toDouble() / img.width, c.height.toDouble() / img.height)
|
||||||
|
val newW = (img.width * scale).toInt()
|
||||||
|
val newH = (img.height * scale).toInt()
|
||||||
|
val x = (c.width - newW) / 2
|
||||||
|
val y = (c.height - newH) / 2
|
||||||
|
g2d.drawImage(img, x, y, newW, newH, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.restore()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
26
plugins/bg/src/main/kotlin/app/termora/plugins/bg/BGI18n.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.AbstractI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object BGI18n : AbstractI18n() {
|
||||||
|
private val log = LoggerFactory.getLogger(BGI18n::class.java)
|
||||||
|
private val myBundle by lazy {
|
||||||
|
val bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault(), BGI18n::class.java.classLoader)
|
||||||
|
if (log.isInfoEnabled) {
|
||||||
|
log.info("I18n: {}", bundle.baseBundleName ?: "null")
|
||||||
|
}
|
||||||
|
return@lazy bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBundle(): ResourceBundle {
|
||||||
|
return myBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.ApplicationRunnerExtension
|
||||||
|
import app.termora.GlassPaneAwareExtension
|
||||||
|
import app.termora.GlassPaneExtension
|
||||||
|
import app.termora.SettingsOptionExtension
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
|
||||||
|
class BGPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(GlassPaneExtension::class.java) { BGGlassPaneExtension.instance }
|
||||||
|
support.addExtension(SettingsOptionExtension::class.java) { BackgroundSettingsOptionExtension.instance }
|
||||||
|
support.addExtension(ApplicationRunnerExtension::class.java) { BackgroundManager.getInstance() }
|
||||||
|
support.addExtension(GlassPaneAwareExtension::class.java) { BackgroundManager.getInstance() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Customize Background"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal class BackgroundManager private constructor() : Disposable, GlassPaneAwareExtension,
|
||||||
|
ApplicationRunnerExtension {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundManager::class.java)
|
||||||
|
fun getInstance(): BackgroundManager {
|
||||||
|
return ApplicationScope.Companion.forApplicationScope()
|
||||||
|
.getOrCreate(BackgroundManager::class) { BackgroundManager() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bufferedImage: BufferedImage? = null
|
||||||
|
private var imageFilepath = StringUtils.EMPTY
|
||||||
|
private val glassPanes = mutableListOf<WeakReference<JComponent>>()
|
||||||
|
|
||||||
|
|
||||||
|
fun setBackgroundImage(url: String) {
|
||||||
|
clearBackgroundImage()
|
||||||
|
Appearance.backgroundImage = url
|
||||||
|
refreshBackgroundImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundImage(): BufferedImage? {
|
||||||
|
val bg = doGetBackgroundImage()
|
||||||
|
if (bg == null) {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (JPopupMenu.getDefaultLightWeightPopupEnabled()) {
|
||||||
|
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bg
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doGetBackgroundImage(): BufferedImage? {
|
||||||
|
synchronized(this) {
|
||||||
|
return bufferedImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBackgroundImage() {
|
||||||
|
synchronized(this) {
|
||||||
|
bufferedImage = null
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
Appearance.backgroundImage = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshBackgroundImage() {
|
||||||
|
val backgroundImage = Appearance.backgroundImage
|
||||||
|
if (backgroundImage.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var file: File? = null
|
||||||
|
|
||||||
|
// 从网络下载
|
||||||
|
if (backgroundImage.startsWith("http://") || backgroundImage.startsWith("https://")) {
|
||||||
|
file = Application.httpClient.newCall(
|
||||||
|
Request.Builder().get()
|
||||||
|
.url(backgroundImage).build()
|
||||||
|
).execute().use { response ->
|
||||||
|
val tempFile = File(Application.getTemporaryDir(), randomUUID())
|
||||||
|
if (response.isSuccessful.not()) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Request {} failed with code {}", backgroundImage, response.code)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val body = response.body
|
||||||
|
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||||
|
IOUtils.closeQuietly(body)
|
||||||
|
return@use tempFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backgroundImageFile = File(backgroundImage)
|
||||||
|
if (backgroundImageFile.isDirectory) {
|
||||||
|
val files = FileUtils.listFiles(backgroundImageFile, arrayOf("png", "jpg", "jpeg"), false)
|
||||||
|
if (files.isNotEmpty()) {
|
||||||
|
for (i in 0 until files.size) {
|
||||||
|
file = files.randomOrNull()
|
||||||
|
if (file == null) break
|
||||||
|
if (file.absolutePath == imageFilepath) continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
synchronized(this) {
|
||||||
|
imageFilepath = StringUtils.EMPTY
|
||||||
|
bufferedImage = null
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (backgroundImageFile.isFile) {
|
||||||
|
file = backgroundImageFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null || imageFilepath == file.absolutePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedImage = file.inputStream().use { ImageIO.read(it) }
|
||||||
|
imageFilepath = file.absolutePath
|
||||||
|
|
||||||
|
refreshGlassPanes()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshGlassPanes() {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
glassPanes.removeIf {
|
||||||
|
val glassPane = it.get()
|
||||||
|
glassPane?.repaint()
|
||||||
|
glassPane == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setGlassPane(window: Window, glassPane: JComponent) {
|
||||||
|
glassPanes.add(WeakReference(glassPane))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ready() {
|
||||||
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
while (isActive) {
|
||||||
|
runCatching { refreshBackgroundImage() }.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Refresh failed", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(max(Appearance.interval, 30).seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatButton
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
|
||||||
|
class BackgroundOption : JPanel(BorderLayout()), OptionsPane.PluginOption {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(BackgroundOption::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||||
|
|
||||||
|
val backgroundImageTextField = OutlineTextField()
|
||||||
|
val fillModeComboBox = OutlineComboBox<FillMode>()
|
||||||
|
val intervalSpinner = NumberSpinner(360, minimum = 30, maximum = 86400)
|
||||||
|
|
||||||
|
private val backgroundButton = JButton(Icons.folder)
|
||||||
|
private val backgroundClearButton = FlatButton()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
fillModeComboBox.addItem(FillMode.STRETCH)
|
||||||
|
fillModeComboBox.addItem(FillMode.FIT)
|
||||||
|
fillModeComboBox.addItem(FillMode.CENTER)
|
||||||
|
fillModeComboBox.addItem(FillMode.TILE)
|
||||||
|
|
||||||
|
fillModeComboBox.selectedItem = runCatching { FillMode.valueOf(Appearance.fillMode) }
|
||||||
|
.getOrNull() ?: FillMode.STRETCH
|
||||||
|
|
||||||
|
fillModeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component? {
|
||||||
|
var text = value?.toString()
|
||||||
|
|
||||||
|
if (value == FillMode.STRETCH) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.stretch")
|
||||||
|
} else if (value == FillMode.FIT) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.fit")
|
||||||
|
} else if (value == FillMode.CENTER) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.center")
|
||||||
|
} else if (value == FillMode.TILE) {
|
||||||
|
text = BGI18n.getString("termora.plugins.bg.fill-mode.tile")
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundImageTextField.isEditable = false
|
||||||
|
backgroundImageTextField.trailingComponent = backgroundButton
|
||||||
|
backgroundImageTextField.text = Appearance.backgroundImage
|
||||||
|
backgroundImageTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
backgroundClearButton.isFocusable = false
|
||||||
|
backgroundClearButton.isEnabled = backgroundImageTextField.text.isNotBlank()
|
||||||
|
backgroundClearButton.icon = Icons.delete
|
||||||
|
backgroundClearButton.buttonType = FlatButton.ButtonType.toolBarButton
|
||||||
|
|
||||||
|
intervalSpinner.value = Appearance.interval
|
||||||
|
|
||||||
|
add(getFormPanel(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
backgroundButton.addActionListener {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.osxAllowedFileTypes = listOf("png", "jpg", "jpeg")
|
||||||
|
chooser.allowsMultiSelection = false
|
||||||
|
chooser.win32Filters.add(Pair("Image files", listOf("png", "jpg", "jpeg")))
|
||||||
|
chooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES
|
||||||
|
chooser.showOpenDialog(owner).thenAccept {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
onSelectedBackgroundImage(it.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundClearButton.addActionListener {
|
||||||
|
BackgroundManager.getInstance().clearBackgroundImage()
|
||||||
|
backgroundImageTextField.text = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalSpinner.addChangeListener {
|
||||||
|
val value = intervalSpinner.value
|
||||||
|
if (value is Int) {
|
||||||
|
Appearance.interval = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fillModeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
Appearance.fillMode = fillModeComboBox.selectedItem?.toString() ?: FillMode.STRETCH.name
|
||||||
|
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
|
SwingUtilities.invokeLater { SwingUtilities.updateComponentTreeUI(frame) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelectedBackgroundImage(file: File) {
|
||||||
|
try {
|
||||||
|
if (file.isFile) {
|
||||||
|
val destFile = FileUtils.getFile(Application.getBaseDataDir(), "background", file.name)
|
||||||
|
FileUtils.forceMkdirParent(destFile)
|
||||||
|
FileUtils.deleteQuietly(destFile)
|
||||||
|
FileUtils.copyFile(file, destFile, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
BackgroundManager.getInstance().setBackgroundImage(destFile.absolutePath)
|
||||||
|
} else if (file.isDirectory) {
|
||||||
|
BackgroundManager.getInstance().setBackgroundImage(file.absolutePath)
|
||||||
|
}
|
||||||
|
backgroundImageTextField.text = file.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
OptionPane.showMessageDialog(
|
||||||
|
owner,
|
||||||
|
ExceptionUtils.getRootCauseMessage(e),
|
||||||
|
messageType = JOptionPane.ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.imageGray
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return BGI18n.getString("termora.plugins.bg.background-image")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getFormPanel(): JPanel {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val builder = FormBuilder.create().layout(layout)
|
||||||
|
val bgClearBox = Box.createHorizontalBox()
|
||||||
|
bgClearBox.add(backgroundClearButton)
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.background-image")}:").xy(1, rows)
|
||||||
|
.add(backgroundImageTextField).xy(3, rows)
|
||||||
|
.add(bgClearBox).xy(5, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.fill-mode")}:").xy(1, rows)
|
||||||
|
.add(fillModeComboBox).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
builder.add("${BGI18n.getString("termora.plugins.bg.interval")}:").xy(1, rows)
|
||||||
|
.add(intervalSpinner).xy(3, rows)
|
||||||
|
.apply { rows += step }
|
||||||
|
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
import app.termora.OptionsPane
|
||||||
|
import app.termora.SettingsOptionExtension
|
||||||
|
|
||||||
|
class BackgroundSettingsOptionExtension private constructor(): SettingsOptionExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { BackgroundSettingsOptionExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSettingsOption(): OptionsPane.Option {
|
||||||
|
return BackgroundOption()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package app.termora.plugins.bg
|
||||||
|
|
||||||
|
enum class FillMode {
|
||||||
|
STRETCH, // 拉伸
|
||||||
|
FIT, // 等比例铺满
|
||||||
|
CENTER, // 居中
|
||||||
|
TILE, // 平铺
|
||||||
|
}
|
||||||
23
plugins/bg/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>bg</id>
|
||||||
|
|
||||||
|
<name>Customize Background</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.bg.BGPlugin</entry>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Customize application background</description>
|
||||||
|
<description language="zh_CN">自定义应用程序背景</description>
|
||||||
|
<description language="zh_TW">自訂應用程式背景</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
6
plugins/bg/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#3574F0"/>
|
||||||
|
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#3574F0"/>
|
||||||
|
<circle cx="10" cy="6" r="1.5" stroke="#3574F0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.5" y="2.5" width="11" height="11" rx="1.5" stroke="#548AF7"/>
|
||||||
|
<path d="M2.5 9.33566L4.1822 7.66899C4.56052 7.29415 5.16625 7.28159 5.55979 7.64043L11.9861 13.5" stroke="#548AF7"/>
|
||||||
|
<circle cx="10" cy="6" r="1.5" stroke="#548AF7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
7
plugins/bg/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
termora.plugins.bg.interval=Interval
|
||||||
|
termora.plugins.bg.fill-mode=Fill Mode
|
||||||
|
termora.plugins.bg.fill-mode.stretch=Stretch
|
||||||
|
termora.plugins.bg.fill-mode.fit=Fit
|
||||||
|
termora.plugins.bg.fill-mode.center=Center
|
||||||
|
termora.plugins.bg.fill-mode.tile=Tile
|
||||||
|
termora.plugins.bg.background-image=Background Image
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
termora.plugins.bg.background-image=背景图
|
||||||
|
termora.plugins.bg.interval=切换间隔
|
||||||
|
|
||||||
|
termora.plugins.bg.fill-mode=填充模式
|
||||||
|
termora.plugins.bg.fill-mode.stretch=拉伸
|
||||||
|
termora.plugins.bg.fill-mode.fit=适合
|
||||||
|
termora.plugins.bg.fill-mode.center=居中
|
||||||
|
termora.plugins.bg.fill-mode.tile=平铺
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
termora.plugins.bg.background-image=背景圖
|
||||||
|
termora.plugins.bg.interval=切換間隔
|
||||||
|
|
||||||
|
termora.plugins.bg.fill-mode=填充模式
|
||||||
|
termora.plugins.bg.fill-mode.stretch=拉伸
|
||||||
|
termora.plugins.bg.fill-mode.fit=適合
|
||||||
|
termora.plugins.bg.fill-mode.center=居中
|
||||||
|
termora.plugins.bg.fill-mode.tile=平鋪
|
||||||
89
plugins/common.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||||
|
|
||||||
|
|
||||||
|
tasks.withType<Jar> {
|
||||||
|
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
"Implementation-Title" to project.name,
|
||||||
|
"Implementation-Version" to project.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
from("${rootProject.projectDir}/plugins/LICENSE") {
|
||||||
|
into("META-INF")
|
||||||
|
}
|
||||||
|
|
||||||
|
from("${rootProject.projectDir}/plugins/THIRDPARTY") {
|
||||||
|
into("META-INF")
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveBaseName.set("${project.name}-${rootProject.version}")
|
||||||
|
destinationDirectory.set(file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Copy>("processResources") {
|
||||||
|
filesMatching("META-INF/plugin.xml") {
|
||||||
|
expand(
|
||||||
|
"projectName" to project.name,
|
||||||
|
"projectVersion" to project.version,
|
||||||
|
"rootProjectVersion" to rootProject.version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("copy-dependencies") {
|
||||||
|
from(configurations.getByName("runtimeClasspath").filterNot {
|
||||||
|
it.name.startsWith("kotlin-stdlib") || it.name.startsWith("annotations")
|
||||||
|
})
|
||||||
|
into("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("build") {
|
||||||
|
dependsOn("copy-dependencies")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("run-plugin") {
|
||||||
|
dependsOn("build")
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||||
|
|
||||||
|
val runtimeCompileOnly by configurations.creating { extendsFrom(configurations.getByName("compileOnly")) }
|
||||||
|
val mainClass = "app.termora.MainKt"
|
||||||
|
val executable = System.getProperty("java.home") + "/bin/java"
|
||||||
|
val classpath = (configurations.getByName("compileClasspath") + configurations.getByName("runtimeClasspath")
|
||||||
|
+ runtimeCompileOnly).joinToString(if (os.isWindows) ";" else ":")
|
||||||
|
val commands = mutableListOf<String>(executable)
|
||||||
|
commands.add("-Dapp-version=${rootProject.version}")
|
||||||
|
commands.add("--add-exports java.base/sun.nio.ch=ALL-UNNAMED")
|
||||||
|
if (os.isMacOsX) {
|
||||||
|
// NSWindow
|
||||||
|
commands.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||||
|
commands.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||||
|
commands.add("--add-exports java.desktop/com.apple.eawt=ALL-UNNAMED")
|
||||||
|
commands.add("-Dapple.awt.application.appearance=system")
|
||||||
|
}
|
||||||
|
commands.addAll(listOf("-cp", classpath, mainClass))
|
||||||
|
|
||||||
|
exec {
|
||||||
|
commandLine = commands
|
||||||
|
environment(
|
||||||
|
"TERMORA_PLUGIN_DIRECTORY" to file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/"),
|
||||||
|
"TERMORA_BASE_DATA_DIR" to "${layout.buildDirectory.get().asFile.absolutePath}/data",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("clean") {
|
||||||
|
doLast {
|
||||||
|
file("${rootProject.layout.buildDirectory.get().asFile.absolutePath}/plugins/${project.name}").deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
16
plugins/cos/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.4"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
implementation("com.qcloud:cos_api:5.6.259")
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.AuthenticationType
|
||||||
|
import app.termora.Proxy
|
||||||
|
import app.termora.ProxyType
|
||||||
|
import com.qcloud.cos.COSClient
|
||||||
|
import com.qcloud.cos.ClientConfig
|
||||||
|
import com.qcloud.cos.auth.BasicCOSCredentials
|
||||||
|
import com.qcloud.cos.model.Bucket
|
||||||
|
import com.qcloud.cos.region.Region
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
class COSClientHandler(
|
||||||
|
private val cred: BasicCOSCredentials,
|
||||||
|
private val proxy: Proxy,
|
||||||
|
val buckets: List<Bucket>
|
||||||
|
) : Closeable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createCOSClient(cred: BasicCOSCredentials, region: String, proxy: Proxy): COSClient {
|
||||||
|
val clientConfig = ClientConfig()
|
||||||
|
if (region.isNotBlank()) {
|
||||||
|
clientConfig.region = Region(region)
|
||||||
|
}
|
||||||
|
clientConfig.isPrintShutdownStackTrace = false
|
||||||
|
if (proxy.type == ProxyType.HTTP) {
|
||||||
|
clientConfig.httpProxyIp = proxy.host
|
||||||
|
clientConfig.httpProxyPort = proxy.port
|
||||||
|
if (proxy.authenticationType == AuthenticationType.Password) {
|
||||||
|
clientConfig.proxyPassword = proxy.password
|
||||||
|
clientConfig.proxyUsername = proxy.username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return COSClient(cred, clientConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: Region
|
||||||
|
* value: Client
|
||||||
|
*/
|
||||||
|
private val clients = mutableMapOf<String, COSClient>()
|
||||||
|
private val closed = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun getClientForBucket(bucket: String): COSClient {
|
||||||
|
if (closed.get()) throw IllegalStateException("Client already closed")
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
val bucket = buckets.first { it.name == bucket }
|
||||||
|
if (clients.containsKey(bucket.location)) {
|
||||||
|
return clients.getValue(bucket.location)
|
||||||
|
}
|
||||||
|
clients[bucket.location] = createCOSClient(cred, bucket.location, proxy)
|
||||||
|
return clients.getValue(bucket.location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
if (closed.compareAndSet(false, true)) {
|
||||||
|
synchronized(this) {
|
||||||
|
clients.forEach { it.value.shutdown() }
|
||||||
|
clients.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key: region
|
||||||
|
*/
|
||||||
|
class COSFileSystem(private val clientHandler: COSClientHandler) :
|
||||||
|
S3FileSystem(COSFileSystemProvider(clientHandler)) {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(clientHandler)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileAttributes
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import com.qcloud.cos.model.ListObjectsRequest
|
||||||
|
import com.qcloud.cos.model.ObjectMetadata
|
||||||
|
import com.qcloud.cos.model.PutObjectRequest
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.io.PipedInputStream
|
||||||
|
import java.io.PipedOutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
|
class COSFileSystemProvider(private val clientHandler: COSClientHandler) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "cos"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
return createStreamer(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val client = clientHandler.getClientForBucket(path.bucketName)
|
||||||
|
return client.getObject(path.bucketName, path.objectName).objectContent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStreamer(path: S3Path): OutputStream {
|
||||||
|
val pis = PipedInputStream()
|
||||||
|
val pos = PipedOutputStream(pis)
|
||||||
|
val exception = AtomicReference<Throwable>()
|
||||||
|
|
||||||
|
val thread = Thread.ofVirtual().start {
|
||||||
|
try {
|
||||||
|
val client = clientHandler.getClientForBucket(path.bucketName)
|
||||||
|
client.putObject(PutObjectRequest(path.bucketName, path.objectName, pis, ObjectMetadata()))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
exception.set(e)
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(pis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
val exception = exception.get()
|
||||||
|
if (exception != null) throw exception
|
||||||
|
pos.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
pos.close()
|
||||||
|
if (thread.isAlive) thread.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
|
||||||
|
// root
|
||||||
|
if (path.isRoot) {
|
||||||
|
for (bucket in clientHandler.buckets) {
|
||||||
|
val p = path.resolve(bucket.name)
|
||||||
|
p.attributes = S3FileAttributes(
|
||||||
|
directory = true,
|
||||||
|
lastModifiedTime = bucket.creationDate.toInstant().toEpochMilli()
|
||||||
|
)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextMarker = StringUtils.EMPTY
|
||||||
|
val maxKeys = 100
|
||||||
|
val bucketName = path.bucketName
|
||||||
|
while (true) {
|
||||||
|
val request = ListObjectsRequest()
|
||||||
|
.withBucketName(bucketName)
|
||||||
|
.withMaxKeys(maxKeys)
|
||||||
|
.withDelimiter(path.fileSystem.separator)
|
||||||
|
|
||||||
|
if (path.objectName.isNotBlank()) request.withPrefix(path.objectName + path.fileSystem.separator)
|
||||||
|
if (nextMarker.isNotBlank()) request.withMarker(nextMarker)
|
||||||
|
|
||||||
|
|
||||||
|
val objectListing = clientHandler.getClientForBucket(bucketName).listObjects(request)
|
||||||
|
for (e in objectListing.commonPrefixes) {
|
||||||
|
val p = path.bucket.resolve(e)
|
||||||
|
p.attributes = p.attributes.copy(directory = true)
|
||||||
|
delete(p)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (e in objectListing.objectSummaries) {
|
||||||
|
val p = path.bucket.resolve(e.key)
|
||||||
|
p.attributes = p.attributes.copy(
|
||||||
|
regularFile = true, size = e.size,
|
||||||
|
lastModifiedTime = e.lastModified.time
|
||||||
|
)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectListing.isTruncated.not()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMarker = objectListing.nextMarker
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.addAll(directories[path.absolutePathString()] ?: emptyList())
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
if (isDirectory.not())
|
||||||
|
clientHandler.getClientForBucket(path.bucketName).deleteObject(path.bucketName, path.objectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
try {
|
||||||
|
val client = clientHandler.getClientForBucket(path.bucketName)
|
||||||
|
if (client.doesObjectExist(path.bucketName, path.objectName).not()) {
|
||||||
|
throw NoSuchFileException(path.objectName)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is NoSuchFileException) throw e
|
||||||
|
throw NoSuchFileException(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class COSHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val proxyOption = BasicProxyOption(listOf(ProxyType.HTTP))
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = COSProtocolProvider.PROTOCOL
|
||||||
|
val port = 0
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"cos.delimiter" to generalOption.delimiterTextField.text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.delimiterTextField.text = host.options.extras["cos.delimiter"] ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.authentication.type == AuthenticationType.Password) {
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(c: JComponent) {
|
||||||
|
selectOptionJComponent(c)
|
||||||
|
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
c.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
|
||||||
|
// val regionComboBox = OutlineComboBox<String>()
|
||||||
|
val delimiterTextField = OutlineTextField(128)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
/*regionComboBox.addItem("ap-beijing-1")
|
||||||
|
regionComboBox.addItem("ap-beijing")
|
||||||
|
regionComboBox.addItem("ap-nanjing")
|
||||||
|
regionComboBox.addItem("ap-shanghai")
|
||||||
|
regionComboBox.addItem("ap-guangzhou")
|
||||||
|
regionComboBox.addItem("ap-chengdu")
|
||||||
|
regionComboBox.addItem("ap-chongqing")
|
||||||
|
regionComboBox.addItem("ap-shenzhen-fsi")
|
||||||
|
regionComboBox.addItem("ap-shanghai-fsi")
|
||||||
|
regionComboBox.addItem("ap-beijing-fsi")
|
||||||
|
|
||||||
|
regionComboBox.addItem("ap-hongkong")
|
||||||
|
regionComboBox.addItem("ap-singapore")
|
||||||
|
regionComboBox.addItem("ap-jakarta")
|
||||||
|
regionComboBox.addItem("ap-seoul")
|
||||||
|
regionComboBox.addItem("ap-bangkok")
|
||||||
|
regionComboBox.addItem("ap-tokyo")
|
||||||
|
regionComboBox.addItem("na-siliconvalley")
|
||||||
|
regionComboBox.addItem("na-ashburn")
|
||||||
|
regionComboBox.addItem("sa-saopaulo")
|
||||||
|
regionComboBox.addItem("eu-frankfurt")
|
||||||
|
|
||||||
|
regionComboBox.isEditable = true*/
|
||||||
|
|
||||||
|
delimiterTextField.text = "/"
|
||||||
|
delimiterTextField.isEditable = false
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("SecretId:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("SecretKey:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("Delimiter:").xy(1, rows)
|
||||||
|
.add(delimiterTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.transport.sftp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.PaidPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class COSPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { COSProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { COSProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "Tencent COS"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class COSProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = COSHostOptionsPane()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return pane.getHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return pane.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return COSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
|
return COSProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import com.qcloud.cos.ClientConfig
|
||||||
|
import com.qcloud.cos.auth.BasicCOSCredentials
|
||||||
|
import com.qcloud.cos.model.Bucket
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
|
||||||
|
class COSProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolProvider() }
|
||||||
|
const val PROTOCOL = "COS"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.tencent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val host = requester.host
|
||||||
|
val secretId = host.username
|
||||||
|
val secretKey = host.authentication.password
|
||||||
|
val cred = BasicCOSCredentials(secretId, secretKey)
|
||||||
|
val clientConfig = ClientConfig()
|
||||||
|
|
||||||
|
clientConfig.isPrintShutdownStackTrace = false
|
||||||
|
val cosClient = COSClientHandler.createCOSClient(cred, StringUtils.EMPTY, host.proxy)
|
||||||
|
val buckets: List<Bucket>
|
||||||
|
|
||||||
|
try {
|
||||||
|
buckets = cosClient.listBuckets()
|
||||||
|
} finally {
|
||||||
|
cosClient.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultPath = host.options.sftpDefaultDirectory
|
||||||
|
val fs = COSFileSystem(COSClientHandler(cred, host.proxy, buckets))
|
||||||
|
return PathHandler(fs, fs.getPath(defaultPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class COSProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { COSProtocolProviderExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return COSProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
25
plugins/cos/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>cos</id>
|
||||||
|
|
||||||
|
<name>Tencent COS</name>
|
||||||
|
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.cos.COSPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to Tencent COS</description>
|
||||||
|
<description language="zh_CN">支持连接到腾讯云对象存储</description>
|
||||||
|
<description language="zh_TW">支援連接到騰訊雲物件存儲</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/cos/src/main/resources/META-INF/pluginIcon.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
19
plugins/editor/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
project.version = "0.0.8"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
|
||||||
|
implementation("com.fifesoft:languagesupport:3.4.0")
|
||||||
|
implementation("com.fifesoft:autocomplete:3.3.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.EnableManager
|
||||||
|
import app.termora.OptionPane
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import javax.swing.JOptionPane
|
||||||
|
import javax.swing.UIManager
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class EditorFrame(private val file: Path, private val owner: Window, private val disposable: Disposable) : JFrame() {
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val disposed = AtomicBoolean()
|
||||||
|
private val filepath = File(file.absolutePathString())
|
||||||
|
private val frame get() = this
|
||||||
|
private val editorPanel = EditorPanel(this, filepath)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvent() {
|
||||||
|
|
||||||
|
Disposer.register(disposable, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
if (disposed.compareAndSet(false, true)) frame.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowClosed(e: WindowEvent) {
|
||||||
|
if (disposed.compareAndSet(false, true)) Disposer.dispose(disposable)
|
||||||
|
enableManager.setFlag("Plugins.editor.dialog.width", width)
|
||||||
|
enableManager.setFlag("Plugins.editor.dialog.height", height)
|
||||||
|
enableManager.setFlag("Plugins.editor.dialog.extendedState", extendedState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun windowClosing(e: WindowEvent?) {
|
||||||
|
if (editorPanel.changes()) {
|
||||||
|
if (OptionPane.showConfirmDialog(
|
||||||
|
frame,
|
||||||
|
EditorI18n.getString("termora.plugins.editor.not-save"),
|
||||||
|
optionType = JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
) == JOptionPane.OK_OPTION
|
||||||
|
) {
|
||||||
|
frame.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
frame.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||||
|
val state = enableManager.getFlag("Plugins.editor.dialog.extendedState", 0)
|
||||||
|
|
||||||
|
if ((state and MAXIMIZED_BOTH) == MAXIMIZED_BOTH) {
|
||||||
|
frame.setLocationRelativeTo(null)
|
||||||
|
frame.extendedState = state
|
||||||
|
} else {
|
||||||
|
val mySize = size
|
||||||
|
mySize.width = max(enableManager.getFlag("Plugins.editor.dialog.width", mySize.width), mySize.width)
|
||||||
|
mySize.height = max(enableManager.getFlag("Plugins.editor.dialog.height", mySize.height), mySize.height)
|
||||||
|
size = mySize
|
||||||
|
setLocationRelativeTo(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
title = file.name
|
||||||
|
iconImages = owner.iconImages
|
||||||
|
defaultCloseOperation = DO_NOTHING_ON_CLOSE
|
||||||
|
|
||||||
|
rootPane.contentPane.layout = BorderLayout()
|
||||||
|
rootPane.contentPane.add(editorPanel, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
object EditorI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(EditorI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.database.DatabaseManager
|
||||||
|
import com.formdev.flatlaf.FlatLaf
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.dom4j.io.OutputFormat
|
||||||
|
import org.dom4j.io.SAXReader
|
||||||
|
import org.dom4j.io.XMLWriter
|
||||||
|
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
|
||||||
|
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
|
||||||
|
import org.fife.ui.rsyntaxtextarea.Theme
|
||||||
|
import org.fife.ui.rtextarea.RTextScrollPane
|
||||||
|
import org.fife.ui.rtextarea.SearchContext
|
||||||
|
import org.fife.ui.rtextarea.SearchEngine
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Insets
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.WindowAdapter
|
||||||
|
import java.awt.event.WindowEvent
|
||||||
|
import java.io.File
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.SwingConstants.VERTICAL
|
||||||
|
import javax.swing.event.DocumentEvent
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class EditorPanel(private val window: JFrame, private val file: File) : JPanel(BorderLayout()) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
||||||
|
private val saveIcon = DynamicIcon(
|
||||||
|
"icons/save.svg", "icons/save_dark.svg",
|
||||||
|
loader = EditorPlugin::class.java.classLoader
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var text = file.readText(Charsets.UTF_8)
|
||||||
|
private val layeredPane = LayeredPane()
|
||||||
|
|
||||||
|
private val textArea = RSyntaxTextArea()
|
||||||
|
private val scrollPane = RTextScrollPane(textArea)
|
||||||
|
private val findPanel = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||||
|
private val searchTextField = FlatTextField()
|
||||||
|
private val closeFindPanelBtn = JButton(Icons.close)
|
||||||
|
private val nextBtn = JButton(Icons.down)
|
||||||
|
private val prevBtn = JButton(Icons.up)
|
||||||
|
private val context = SearchContext()
|
||||||
|
private val softWrapBtn = JToggleButton(Icons.softWrap)
|
||||||
|
private val saveBtn = JButton(saveIcon)
|
||||||
|
private val scrollUpBtn = JButton(Icons.scrollUp)
|
||||||
|
private val scrollEndBtn = JButton(Icons.scrollDown)
|
||||||
|
private val prettyBtn = JButton(Icons.reformatCode)
|
||||||
|
|
||||||
|
private val enableManager get() = EnableManager.getInstance()
|
||||||
|
private val prettyJson = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
textArea.font = textArea.font.deriveFont(DatabaseManager.getInstance().terminal.fontSize.toFloat())
|
||||||
|
textArea.text = text
|
||||||
|
textArea.antiAliasingEnabled = true
|
||||||
|
softWrapBtn.isSelected = enableManager.getFlag("Plugins.editor.softWrap", false)
|
||||||
|
|
||||||
|
val theme = if (FlatLaf.isLafDark())
|
||||||
|
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/dark.xml"))
|
||||||
|
else
|
||||||
|
Theme.load(javaClass.getResourceAsStream("/org/fife/ui/rsyntaxtextarea/themes/idea.xml"))
|
||||||
|
|
||||||
|
theme.apply(textArea)
|
||||||
|
|
||||||
|
val extension = FilenameUtils.getExtension(file.name)?.lowercase()
|
||||||
|
textArea.syntaxEditingStyle = when (extension) {
|
||||||
|
"java" -> SyntaxConstants.SYNTAX_STYLE_JAVA
|
||||||
|
"kt" -> SyntaxConstants.SYNTAX_STYLE_KOTLIN
|
||||||
|
"properties" -> SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE
|
||||||
|
"cpp", "c++" -> SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS
|
||||||
|
"c" -> SyntaxConstants.SYNTAX_STYLE_C
|
||||||
|
"cs" -> SyntaxConstants.SYNTAX_STYLE_CSHARP
|
||||||
|
"css" -> SyntaxConstants.SYNTAX_STYLE_CSS
|
||||||
|
"html", "htm", "htmlx" -> SyntaxConstants.SYNTAX_STYLE_HTML
|
||||||
|
"js" -> SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT
|
||||||
|
"ts" -> SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT
|
||||||
|
"xml", "svg" -> SyntaxConstants.SYNTAX_STYLE_XML
|
||||||
|
"yaml", "yml" -> SyntaxConstants.SYNTAX_STYLE_YAML
|
||||||
|
"sh", "shell" -> SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL
|
||||||
|
"sql" -> SyntaxConstants.SYNTAX_STYLE_SQL
|
||||||
|
"bat" -> SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH
|
||||||
|
"py" -> SyntaxConstants.SYNTAX_STYLE_PYTHON
|
||||||
|
"php" -> SyntaxConstants.SYNTAX_STYLE_PHP
|
||||||
|
"lua" -> SyntaxConstants.SYNTAX_STYLE_LUA
|
||||||
|
"less" -> SyntaxConstants.SYNTAX_STYLE_LESS
|
||||||
|
"jsp" -> SyntaxConstants.SYNTAX_STYLE_JSP
|
||||||
|
"json" -> SyntaxConstants.SYNTAX_STYLE_JSON
|
||||||
|
"ini" -> SyntaxConstants.SYNTAX_STYLE_INI
|
||||||
|
"hosts" -> SyntaxConstants.SYNTAX_STYLE_HOSTS
|
||||||
|
"go" -> SyntaxConstants.SYNTAX_STYLE_GO
|
||||||
|
"dtd" -> SyntaxConstants.SYNTAX_STYLE_DTD
|
||||||
|
"dart" -> SyntaxConstants.SYNTAX_STYLE_DART
|
||||||
|
"csv" -> SyntaxConstants.SYNTAX_STYLE_CSV
|
||||||
|
"md" -> SyntaxConstants.SYNTAX_STYLE_MARKDOWN
|
||||||
|
else -> SyntaxConstants.SYNTAX_STYLE_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有 JSON 才可以格式化
|
||||||
|
prettyBtn.isVisible = textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON ||
|
||||||
|
textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML
|
||||||
|
|
||||||
|
textArea.discardAllEdits()
|
||||||
|
|
||||||
|
scrollPane.border = BorderFactory.createMatteBorder(0, 0, 0, 1, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
findPanel.isVisible = false
|
||||||
|
findPanel.isOpaque = true
|
||||||
|
findPanel.background = DynamicColor("window")
|
||||||
|
|
||||||
|
searchTextField.background = findPanel.background
|
||||||
|
searchTextField.padding = Insets(0, 4, 0, 0)
|
||||||
|
searchTextField.border = BorderFactory.createEmptyBorder()
|
||||||
|
|
||||||
|
findPanel.add(searchTextField)
|
||||||
|
findPanel.add(prevBtn)
|
||||||
|
findPanel.add(nextBtn)
|
||||||
|
findPanel.add(closeFindPanelBtn)
|
||||||
|
findPanel.border = BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 1, 1, 0, DynamicColor.BorderColor),
|
||||||
|
BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
toolbar.orientation = VERTICAL
|
||||||
|
toolbar.add(saveBtn)
|
||||||
|
toolbar.add(scrollUpBtn)
|
||||||
|
toolbar.add(prettyBtn)
|
||||||
|
toolbar.add(softWrapBtn)
|
||||||
|
toolbar.add(scrollEndBtn)
|
||||||
|
|
||||||
|
saveBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.save")
|
||||||
|
scrollUpBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.first-line")
|
||||||
|
scrollEndBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.last-line")
|
||||||
|
softWrapBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.soft-wrap")
|
||||||
|
prettyBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.format")
|
||||||
|
|
||||||
|
val viewPanel = JPanel(BorderLayout())
|
||||||
|
viewPanel.add(scrollPane, BorderLayout.CENTER)
|
||||||
|
viewPanel.add(toolbar, BorderLayout.EAST)
|
||||||
|
viewPanel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
|
|
||||||
|
layeredPane.add(findPanel, JLayeredPane.MODAL_LAYER as Any)
|
||||||
|
layeredPane.add(viewPanel, JLayeredPane.DEFAULT_LAYER as Any)
|
||||||
|
|
||||||
|
add(layeredPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
window.addWindowListener(object : WindowAdapter() {
|
||||||
|
override fun windowOpened(e: WindowEvent?) {
|
||||||
|
scrollPane.verticalScrollBar.value = 0
|
||||||
|
window.removeWindowListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
softWrapBtn.addActionListener {
|
||||||
|
enableManager.getFlag("Plugins.editor.softWrap", softWrapBtn.isSelected)
|
||||||
|
textArea.lineWrap = softWrapBtn.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollUpBtn.addActionListener { scrollPane.verticalScrollBar.value = 0 }
|
||||||
|
scrollEndBtn.addActionListener { scrollPane.verticalScrollBar.value = scrollPane.verticalScrollBar.maximum }
|
||||||
|
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_S, toolkit.menuShortcutKeyMaskEx),
|
||||||
|
"Save"
|
||||||
|
)
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx),
|
||||||
|
"Find"
|
||||||
|
)
|
||||||
|
textArea.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_F, toolkit.menuShortcutKeyMaskEx or KeyEvent.SHIFT_DOWN_MASK),
|
||||||
|
"Format"
|
||||||
|
)
|
||||||
|
|
||||||
|
searchTextField.inputMap.put(
|
||||||
|
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||||
|
"Esc"
|
||||||
|
)
|
||||||
|
|
||||||
|
searchTextField.actionMap.put("Esc", object : AbstractAction("Esc") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
textArea.clearMarkAllHighlights()
|
||||||
|
textArea.requestFocusInWindow()
|
||||||
|
findPanel.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
closeFindPanelBtn.addActionListener { searchTextField.actionMap.get("Esc").actionPerformed(it) }
|
||||||
|
|
||||||
|
textArea.actionMap.put("Save", object : AbstractAction("Save") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
file.writeText(textArea.text, Charsets.UTF_8)
|
||||||
|
text = textArea.text
|
||||||
|
window.title = file.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
saveBtn.addActionListener(textArea.actionMap.get("Save"))
|
||||||
|
|
||||||
|
textArea.actionMap.put("Format", object : AbstractAction() {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
format()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.actionMap.put("Find", object : AbstractAction("Find") {
|
||||||
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
|
findPanel.isVisible = true
|
||||||
|
searchTextField.selectAll()
|
||||||
|
searchTextField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
textArea.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
window.title = if (textArea.text.hashCode() != text.hashCode()) {
|
||||||
|
"${file.name} *"
|
||||||
|
} else {
|
||||||
|
file.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
searchTextField.document.addDocumentListener(object : DocumentAdaptor() {
|
||||||
|
override fun changedUpdate(e: DocumentEvent) {
|
||||||
|
search()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
searchTextField.addActionListener { nextBtn.doClick(0) }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
prettyBtn.addActionListener(textArea.actionMap.get("Format"))
|
||||||
|
|
||||||
|
prevBtn.addActionListener { search(false) }
|
||||||
|
nextBtn.addActionListener { search(true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun format() {
|
||||||
|
val vertical = scrollPane.verticalScrollBar.value
|
||||||
|
val horizontal = scrollPane.horizontalScrollBar.value
|
||||||
|
val caretPosition = textArea.caretPosition
|
||||||
|
|
||||||
|
val c = if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_JSON) {
|
||||||
|
runCatching {
|
||||||
|
val json = prettyJson.parseToJsonElement(textArea.text)
|
||||||
|
textArea.text = prettyJson.encodeToString(json)
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (textArea.syntaxEditingStyle == SyntaxConstants.SYNTAX_STYLE_XML) {
|
||||||
|
runCatching {
|
||||||
|
val document = SAXReader().read(StringReader(textArea.text))
|
||||||
|
val sw = StringWriter()
|
||||||
|
val writer = XMLWriter(sw, OutputFormat.createPrettyPrint())
|
||||||
|
writer.write(document)
|
||||||
|
textArea.text = sw.toString()
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
c.onSuccess {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
scrollPane.verticalScrollBar.value = min(
|
||||||
|
vertical,
|
||||||
|
scrollPane.verticalScrollBar.maximum
|
||||||
|
)
|
||||||
|
scrollPane.horizontalScrollBar.value = min(
|
||||||
|
horizontal,
|
||||||
|
scrollPane.horizontalScrollBar.maximum
|
||||||
|
)
|
||||||
|
if (caretPosition >= 0 && caretPosition < textArea.document.length) {
|
||||||
|
textArea.caretPosition = caretPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun search(searchForward: Boolean = true) {
|
||||||
|
textArea.clearMarkAllHighlights()
|
||||||
|
|
||||||
|
|
||||||
|
val text: String = searchTextField.getText()
|
||||||
|
if (text.isEmpty()) return
|
||||||
|
context.searchFor = text
|
||||||
|
context.searchForward = searchForward
|
||||||
|
context.wholeWord = false
|
||||||
|
val result = SearchEngine.find(textArea, context)
|
||||||
|
|
||||||
|
prevBtn.isEnabled = result.markedCount > 0
|
||||||
|
nextBtn.isEnabled = result.markedCount > 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changes() = text != textArea.text
|
||||||
|
|
||||||
|
private inner class LayeredPane : JLayeredPane() {
|
||||||
|
override fun doLayout() {
|
||||||
|
synchronized(treeLock) {
|
||||||
|
for (c in components) {
|
||||||
|
if (c == findPanel) {
|
||||||
|
val height = max(findPanel.preferredSize.height, findPanel.height)
|
||||||
|
val x = width / 2
|
||||||
|
c.setBounds(x, 1, width - x, height)
|
||||||
|
} else {
|
||||||
|
c.setBounds(0, 0, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.Plugin
|
||||||
|
import app.termora.transfer.TransportEditFileExtension
|
||||||
|
|
||||||
|
class EditorPlugin : Plugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(TransportEditFileExtension::class.java) { MyTransportEditFileExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "SFTP File Editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.editor
|
||||||
|
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.transfer.TransportEditFileExtension
|
||||||
|
import java.awt.Window
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
class MyTransportEditFileExtension private constructor() : TransportEditFileExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = MyTransportEditFileExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun edit(owner: Window, path: Path): Disposable {
|
||||||
|
val disposable = Disposer.newDisposable()
|
||||||
|
SwingUtilities.invokeLater { EditorFrame(path, owner, disposable).isVisible = true }
|
||||||
|
return disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
22
plugins/editor/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>editor</id>
|
||||||
|
|
||||||
|
<name>SFTP File Editor</name>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.editor.EditorPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Edit SFTP files using the built-in editor</description>
|
||||||
|
<description language="zh_CN">使用内置编辑器编辑 SFTP 文件</description>
|
||||||
|
<description language="zh_TW">使用內建編輯器編輯 SFTP 文件</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||||
|
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#6C707E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#6C707E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.47774C14.2142 6.80872 13.0333 6.84543 12.2909 7.58786L7 12.8787V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" />
|
||||||
|
<path d="M8.09379 5H13C13.5523 5 14 5.44772 14 6V7.02381C14.3594 7.07711 14.7072 7.22842 15 7.47774V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H7V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L8.09379 5Z" fill="#CED0D6"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4122 8.29497C14.0217 7.90444 13.3885 7.90444 12.998 8.29497L11.6466 9.64633L8 13.2929V16H10.7071L15.7051 11.0021C16.0956 10.6116 16.0956 9.97839 15.7051 9.58786L14.4122 8.29497ZM14 11.2929L14.998 10.295L13.7051 9.00208L12.7071 10L14 11.2929ZM12 10.7072L13.2929 12L10.2929 15H9V13.7072L12 10.7072Z" fill="#CED0D6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?
|
||||||
|
termora.plugins.editor.save=Save
|
||||||
|
termora.plugins.editor.first-line=Jump to first line
|
||||||
|
termora.plugins.editor.last-line=Jump to last line
|
||||||
|
termora.plugins.editor.soft-wrap=Soft-wrap
|
||||||
|
termora.plugins.editor.format=Format
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
|
||||||
|
termora.plugins.editor.save=Сохранить
|
||||||
|
termora.plugins.editor.first-line=Перейти на первую строку
|
||||||
|
termora.plugins.editor.last-line=Перейти на последнюю строку
|
||||||
|
termora.plugins.editor.soft-wrap=Мягкий перенос
|
||||||
|
termora.plugins.editor.format=Формат
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?
|
||||||
|
termora.plugins.editor.save=保存
|
||||||
|
termora.plugins.editor.first-line=跳转到第一行
|
||||||
|
termora.plugins.editor.last-line=跳转到最后一行
|
||||||
|
termora.plugins.editor.soft-wrap=自动换行
|
||||||
|
termora.plugins.editor.format=格式化
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
|
||||||
|
termora.plugins.editor.save=儲存
|
||||||
|
termora.plugins.editor.first-line=跳到第一行
|
||||||
|
termora.plugins.editor.last-line=跳到最後一行
|
||||||
|
termora.plugins.editor.soft-wrap=自動換行
|
||||||
|
termora.plugins.editor.format=格式化
|
||||||
4
plugins/editor/src/main/resources/icons/save.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#6C707E" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
4
plugins/editor/src/main/resources/icons/save_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#CED0D6" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
@@ -0,0 +1,107 @@
|
|||||||
|
package app.termora.plugins.editor;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.*;
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
import org.fife.ui.rtextarea.*;
|
||||||
|
import org.fife.ui.rsyntaxtextarea.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple example showing how to do search and replace in a RSyntaxTextArea.
|
||||||
|
* The toolbar isn't very user-friendly, but this is just to show you how to use
|
||||||
|
* the API.<p>
|
||||||
|
*
|
||||||
|
* This example uses RSyntaxTextArea 2.5.6.
|
||||||
|
*/
|
||||||
|
public class FindAndReplaceDemo extends JFrame implements ActionListener {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private RSyntaxTextArea textArea;
|
||||||
|
private JTextField searchField;
|
||||||
|
private JCheckBox regexCB;
|
||||||
|
private JCheckBox matchCaseCB;
|
||||||
|
|
||||||
|
public FindAndReplaceDemo() {
|
||||||
|
|
||||||
|
JPanel cp = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
textArea = new RSyntaxTextArea(20, 60);
|
||||||
|
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
|
||||||
|
textArea.setCodeFoldingEnabled(true);
|
||||||
|
RTextScrollPane sp = new RTextScrollPane(textArea);
|
||||||
|
cp.add(sp);
|
||||||
|
|
||||||
|
// Create a toolbar with searching options.
|
||||||
|
JToolBar toolBar = new JToolBar();
|
||||||
|
searchField = new JTextField(30);
|
||||||
|
toolBar.add(searchField);
|
||||||
|
final JButton nextButton = new JButton("Find Next");
|
||||||
|
nextButton.setActionCommand("FindNext");
|
||||||
|
nextButton.addActionListener(this);
|
||||||
|
toolBar.add(nextButton);
|
||||||
|
searchField.addActionListener(new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
nextButton.doClick(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JButton prevButton = new JButton("Find Previous");
|
||||||
|
prevButton.setActionCommand("FindPrev");
|
||||||
|
prevButton.addActionListener(this);
|
||||||
|
toolBar.add(prevButton);
|
||||||
|
regexCB = new JCheckBox("Regex");
|
||||||
|
toolBar.add(regexCB);
|
||||||
|
matchCaseCB = new JCheckBox("Match Case");
|
||||||
|
toolBar.add(matchCaseCB);
|
||||||
|
cp.add(toolBar, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
setContentPane(cp);
|
||||||
|
setTitle("Find and Replace Demo");
|
||||||
|
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||||
|
pack();
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
|
||||||
|
// "FindNext" => search forward, "FindPrev" => search backward
|
||||||
|
String command = e.getActionCommand();
|
||||||
|
boolean forward = "FindNext".equals(command);
|
||||||
|
|
||||||
|
// Create an object defining our search parameters.
|
||||||
|
SearchContext context = new SearchContext();
|
||||||
|
String text = searchField.getText();
|
||||||
|
if (text.length() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.setSearchFor(text);
|
||||||
|
context.setMatchCase(matchCaseCB.isSelected());
|
||||||
|
context.setRegularExpression(regexCB.isSelected());
|
||||||
|
context.setSearchForward(forward);
|
||||||
|
context.setWholeWord(false);
|
||||||
|
|
||||||
|
boolean found = SearchEngine.find(textArea, context).wasFound();
|
||||||
|
if (!found) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Text not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Start all Swing applications on the EDT.
|
||||||
|
SwingUtilities.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
String laf = UIManager.getSystemLookAndFeelClassName();
|
||||||
|
UIManager.setLookAndFeel(laf);
|
||||||
|
} catch (Exception e) { /* never happens */ }
|
||||||
|
FindAndReplaceDemo demo = new FindAndReplaceDemo();
|
||||||
|
demo.setVisible(true);
|
||||||
|
demo.textArea.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
plugins/ftp/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.2"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("org.apache.commons:commons-pool2:2.13.0")
|
||||||
|
testImplementation(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
|
||||||
|
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||||
|
|
||||||
|
override fun create(root: String?, names: List<String>): S3Path {
|
||||||
|
val path = FTPPath(this, root, names)
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
path.attributes = path.attributes.copy(directory = true)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(pool)
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import app.termora.keymgr.KeyManager
|
||||||
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import java.awt.event.ComponentAdapter
|
||||||
|
import java.awt.event.ComponentEvent
|
||||||
|
import java.awt.event.ItemEvent
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class FTPHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(proxyOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = FTPProtocolProvider.PROTOCOL
|
||||||
|
val port = generalOption.portTextField.value as Int
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
var proxy = Proxy.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||||
|
proxy = proxy.copy(
|
||||||
|
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||||
|
host = proxyOption.proxyHostTextField.text,
|
||||||
|
username = proxyOption.proxyUsernameTextField.text,
|
||||||
|
password = String(proxyOption.proxyPasswordTextField.password),
|
||||||
|
port = proxyOption.proxyPortTextField.value as Int,
|
||||||
|
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||||
|
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
port = port,
|
||||||
|
host = generalOption.hostTextField.text,
|
||||||
|
username = generalOption.usernameTextField.text,
|
||||||
|
authentication = authentication,
|
||||||
|
proxy = proxy,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.text = host.username
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
|
||||||
|
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||||
|
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||||
|
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||||
|
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||||
|
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||||
|
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||||
|
|
||||||
|
|
||||||
|
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||||
|
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||||
|
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||||
|
.getOrNull() ?: PassiveMode.Local
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
val host = getHost()
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (validateField(generalOption.hostTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||||
|
if (validateField(generalOption.usernameTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateField(generalOption.passwordTextField)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
if (host.proxy.type != ProxyType.No) {
|
||||||
|
if (validateField(proxyOption.proxyHostTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||||
|
if (validateField(proxyOption.proxyUsernameTextField)
|
||||||
|
|| validateField(proxyOption.proxyPasswordTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(c: JComponent) {
|
||||||
|
selectOptionJComponent(c)
|
||||||
|
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
c.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(21)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val usernameTextField = OutlineTextField(128)
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val publicKeyComboBox = OutlineComboBox<String>()
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
|
||||||
|
publicKeyComboBox.isEditable = false
|
||||||
|
|
||||||
|
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = StringUtils.EMPTY
|
||||||
|
if (value is String) {
|
||||||
|
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<*>?,
|
||||||
|
value: Any?,
|
||||||
|
index: Int,
|
||||||
|
isSelected: Boolean,
|
||||||
|
cellHasFocus: Boolean
|
||||||
|
): Component {
|
||||||
|
var text = value?.toString() ?: ""
|
||||||
|
when (value) {
|
||||||
|
AuthenticationType.Password -> {
|
||||||
|
text = "Password"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.PublicKey -> {
|
||||||
|
text = "Public Key"
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationType.KeyboardInteractive -> {
|
||||||
|
text = "Keyboard Interactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.getListCellRendererComponent(
|
||||||
|
list,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
isSelected,
|
||||||
|
cellHasFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||||
|
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||||
|
|
||||||
|
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
addComponentListener(object : ComponentAdapter() {
|
||||||
|
override fun componentResized(e: ComponentEvent) {
|
||||||
|
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||||
|
removeComponentListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authenticationTypeComboBox.addItemListener {
|
||||||
|
if (it.stateChange == ItemEvent.SELECTED) {
|
||||||
|
passwordTextField.isEnabled = authenticationTypeComboBox.selectedItem == AuthenticationType.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.new-host.general")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
remarkTextArea.setFocusTraversalKeys(
|
||||||
|
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||||
|
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||||
|
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
remarkTextArea.rows = 8
|
||||||
|
remarkTextArea.lineWrap = true
|
||||||
|
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||||
|
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||||
|
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||||
|
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||||
|
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||||
|
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||||
|
.xyw(3, rows, 5).apply { rows += step }
|
||||||
|
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||||
|
val defaultDirectoryField = OutlineTextField(255)
|
||||||
|
val charsetComboBox = JComboBox<String>()
|
||||||
|
val passiveComboBox = JComboBox<PassiveMode>()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
|
||||||
|
for (e in Charset.availableCharsets()) {
|
||||||
|
charsetComboBox.addItem(e.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
charsetComboBox.selectedItem = "UTF-8"
|
||||||
|
|
||||||
|
passiveComboBox.addItem(PassiveMode.Local)
|
||||||
|
passiveComboBox.addItem(PassiveMode.Remote)
|
||||||
|
|
||||||
|
add(getCenterComponent(), BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getIcon(isSelected: Boolean): Icon {
|
||||||
|
return Icons.folder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return I18n.getString("termora.transport.sftp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getJComponent(): JComponent {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCenterComponent(): JComponent {
|
||||||
|
val layout = FormLayout(
|
||||||
|
"left:pref, $FORM_MARGIN, default:grow",
|
||||||
|
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rows = 1
|
||||||
|
val step = 2
|
||||||
|
val panel = FormBuilder.create().layout(layout)
|
||||||
|
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||||
|
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||||
|
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||||
|
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||||
|
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||||
|
.build()
|
||||||
|
|
||||||
|
|
||||||
|
return panel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PassiveMode {
|
||||||
|
Local,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object FTPI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String): String {
|
||||||
|
return try {
|
||||||
|
substitutor.replace(getBundle().getString(key))
|
||||||
|
} catch (_: MissingResourceException) {
|
||||||
|
I18n.getString(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
|
||||||
|
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||||
|
override val isBucket: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override val bucketName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override val objectName: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getCustomType(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.plugin.Extension
|
||||||
|
import app.termora.plugin.ExtensionSupport
|
||||||
|
import app.termora.plugin.PaidPlugin
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class FTPPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { FTPProtocolProviderExtension.Companion.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { FTPProtocolHostPanelExtension.Companion.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "FTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = FTPHostOptionsPane()
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
add(pane, BorderLayout.CENTER)
|
||||||
|
Disposer.register(this, pane)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initEvents() {}
|
||||||
|
|
||||||
|
override fun getHost(): Host {
|
||||||
|
return pane.getHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHost(host: Host) {
|
||||||
|
pane.setHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateFields(): Boolean {
|
||||||
|
return pane.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = FTPProtocolHostPanelExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return FTPProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
|
return FTPProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.AuthenticationType
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.ProxyType
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||||
|
import org.apache.commons.pool2.PooledObject
|
||||||
|
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
|
||||||
|
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||||
|
|
||||||
|
val instance = FTPProtocolProvider()
|
||||||
|
const val PROTOCOL = "FTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.ftp
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val host = requester.host
|
||||||
|
|
||||||
|
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||||
|
maxTotal = 12
|
||||||
|
// 与 transfer 最大传输量匹配
|
||||||
|
maxIdle = 6
|
||||||
|
minIdle = 1
|
||||||
|
testOnBorrow = false
|
||||||
|
testWhileIdle = true
|
||||||
|
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||||
|
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||||
|
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||||
|
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||||
|
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||||
|
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||||
|
override fun create(): FTPClient {
|
||||||
|
val client = FTPClient()
|
||||||
|
client.charset = Charset.forName(host.options.encoding)
|
||||||
|
client.controlEncoding = client.charset.name()
|
||||||
|
client.connect(host.host, host.port)
|
||||||
|
if (client.isConnected.not()) {
|
||||||
|
throw IllegalStateException("FTP client is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.proxy.type == ProxyType.HTTP) {
|
||||||
|
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||||
|
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||||
|
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||||
|
host.authentication.password else StringUtils.EMPTY
|
||||||
|
if (client.login(host.username, password).not()) {
|
||||||
|
throw IllegalStateException("Incorrect account or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||||
|
client.enterRemotePassiveMode()
|
||||||
|
} else {
|
||||||
|
client.enterLocalPassiveMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.listHiddenFiles = true
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||||
|
return DefaultPooledObject(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||||
|
val ftp = p.`object`
|
||||||
|
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||||
|
try {
|
||||||
|
p.`object`.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isWarnEnabled) {
|
||||||
|
log.warn(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, config)
|
||||||
|
|
||||||
|
val defaultPath = host.options.sftpDefaultDirectory
|
||||||
|
val fs = FTPFileSystem(ftpClientPool)
|
||||||
|
return PathHandler(fs, fs.getPath(defaultPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance = FTPProtocolProviderExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return FTPProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package app.termora.plugins.ftp
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.net.ftp.FTPClient
|
||||||
|
import org.apache.commons.net.ftp.FTPFile
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.CopyOption
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
|
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "ftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
return createStreamer(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val ftp = pool.borrowObject()
|
||||||
|
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||||
|
return object : InputStream() {
|
||||||
|
override fun read(): Int {
|
||||||
|
return fs.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(fs)
|
||||||
|
ftp.completePendingCommand()
|
||||||
|
pool.returnObject(ftp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStreamer(path: S3Path): OutputStream {
|
||||||
|
val ftp = pool.borrowObject()
|
||||||
|
val os = ftp.storeFileStream(path.absolutePathString())
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
os.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(os)
|
||||||
|
ftp.completePendingCommand()
|
||||||
|
pool.returnObject(ftp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
if (path.exists().not()) {
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
withFtpClient {
|
||||||
|
val files = it.listFiles(path.absolutePathString())
|
||||||
|
for (file in files) {
|
||||||
|
val p = path.resolve(file.name)
|
||||||
|
p.attributes = p.attributes.copy(
|
||||||
|
directory = file.isDirectory,
|
||||||
|
regularFile = file.isFile,
|
||||||
|
size = file.size,
|
||||||
|
lastModifiedTime = file.timestamp.timeInMillis,
|
||||||
|
)
|
||||||
|
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||||
|
paths.add(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||||
|
val perms = mutableSetOf<PosixFilePermission>()
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_READ)
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_READ)
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||||
|
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_READ)
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||||
|
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||||
|
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||||
|
|
||||||
|
return perms
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||||
|
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||||
|
if (source != null && target != null) {
|
||||||
|
withFtpClient {
|
||||||
|
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
withFtpClient {
|
||||||
|
if (isDirectory) {
|
||||||
|
it.rmd(path.absolutePathString())
|
||||||
|
} else {
|
||||||
|
it.deleteFile(path.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
withFtpClient {
|
||||||
|
if (it.cwd(path.absolutePathString()) == 250) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||||
|
val client = pool.borrowObject()
|
||||||
|
return try {
|
||||||
|
block(client)
|
||||||
|
} finally {
|
||||||
|
pool.returnObject(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
plugins/ftp/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>ftp</id>
|
||||||
|
|
||||||
|
<name>FTP</name>
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.ftp.FTPPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to FTP</description>
|
||||||
|
<description language="zh_CN">支持连接到 FTP</description>
|
||||||
|
<description language="zh_TW">支援連接到 FTP</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/ftp/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=Passive Mode
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=被动模式
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.ftp.passive=被動模式
|
||||||
16
plugins/geo/build.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
project.version = "0.0.8"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
compileOnly(project(":"))
|
||||||
|
implementation("com.maxmind.geoip2:geoip2:5.0.0")
|
||||||
|
// https://github.com/hstyi/geolite2
|
||||||
|
implementation("com.github.hstyi:geolite2:v1.0-202510270056")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
|
|
||||||
82
plugins/geo/src/main/kotlin/app/termora/plugins/geo/Geo.kt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package app.termora.plugins.geo
|
||||||
|
|
||||||
|
import app.termora.ApplicationScope
|
||||||
|
import app.termora.Disposable
|
||||||
|
import app.termora.geo.GeoLibrary
|
||||||
|
import com.maxmind.db.CHMCache
|
||||||
|
import com.maxmind.geoip2.DatabaseReader
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.jvm.optionals.getOrNull
|
||||||
|
|
||||||
|
|
||||||
|
internal class Geo private constructor() : Disposable {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(Geo::class.java)
|
||||||
|
|
||||||
|
fun getInstance(): Geo {
|
||||||
|
return ApplicationScope.forApplicationScope()
|
||||||
|
.getOrCreate(Geo::class) { Geo() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val initialized = AtomicBoolean(false)
|
||||||
|
private var reader: DatabaseReader? = null
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
if (isInitialized()) return
|
||||||
|
|
||||||
|
if (initialized.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
val input = GeoLibrary.getInputStream()
|
||||||
|
if (input == null) {
|
||||||
|
throw IllegalStateException("GeoLite2-Country.mmdb not be found")
|
||||||
|
}
|
||||||
|
val locale = Locale.getDefault().toString().replace("_", "-")
|
||||||
|
try {
|
||||||
|
reader = DatabaseReader.Builder(input)
|
||||||
|
.locales(listOf(locale, "en"))
|
||||||
|
.withCache(CHMCache()).build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error("Failed to initialize geo database", e)
|
||||||
|
}
|
||||||
|
initialized.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun country(ip: String): Country? {
|
||||||
|
try {
|
||||||
|
initialize()
|
||||||
|
|
||||||
|
val reader = reader ?: return null
|
||||||
|
val response = reader.tryCountry(InetAddress.getByName(ip)).getOrNull() ?: return null
|
||||||
|
val isoCode = response.country.isoCode
|
||||||
|
var name = response.country.name
|
||||||
|
// 控制名称不要太长,如果太长则使用缩写。例如:United States
|
||||||
|
if (name != null && name.length > 6) name = isoCode
|
||||||
|
return Country(isoCode, name ?: isoCode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isDebugEnabled) {
|
||||||
|
log.error("Failed to initialize geo database", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isInitialized(): Boolean = initialized.get()
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
IOUtils.closeQuietly(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Country(val isoCode: String, val name: String)
|
||||||
|
}
|
||||||